359 changes: 127 additions & 232 deletions Makefile.core.mk

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion Makefile.overrides.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@

.DEFAULT_GOAL := default

# this repo is not yet on the container plan by default
# this repo is not yet on the container plan by default.
#
# This repository has been enabled for BUILD_WITH_CONTAINER=1. Some
# test cases fail within Docker, and Mac + Docker isn't quite perfect.
# For more information see: https://github.com/istio/istio/pull/19322/

BUILD_WITH_CONTAINER ?= 0

ifeq ($(BUILD_WITH_CONTAINER),1)
# create phony targets for the top-level items in the repo
PHONYS := $(shell ls | grep -v Makefile)
.PHONY: $(PHONYS)
$(PHONYS):
@$(MAKE) $@
endif
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/istio/istio)](https://goreportcard.com/report/github.com/istio/istio)
[![GoDoc](https://godoc.org/istio.io/istio?status.svg)](https://godoc.org/istio.io/istio)
[![codecov.io](https://codecov.io/github/istio/istio/coverage.svg?branch=master)](https://codecov.io/github/istio/istio?branch=master)
[![GolangCI](https://golangci.com/badges/github.com/istio/istio.svg)](https://golangci.com/r/github.com/istio/istio)

# Istio

Expand Down
10 changes: 2 additions & 8 deletions bin/codecov.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,9 @@ fi
COVERAGEDIR="$(mktemp -d /tmp/istio_coverage.XXXXXXXXXX)"
mkdir -p "$COVERAGEDIR"

function cleanup() {
make -f Makefile.core.mk localTestEnvCleanup
}

trap cleanup EXIT

# Setup environment needed by some tests.
make -f Makefile.core.mk sync
make -f Makefile.core.mk localTestEnv
make -f Makefile.core.mk init

# coverage test needs to run one package per command.
# This script runs nproc/2 in parallel.
Expand Down Expand Up @@ -118,7 +112,7 @@ parse_skipped_tests
go get github.com/jstemmer/go-junit-report

echo "Code coverage test (concurrency ${MAXPROCS})"
for P in $(go list "${DIR}" | grep -v vendor); do
for P in $(go list "${DIR}" | grep -v vendor | grep -v integration | grep -v e2e); do
if echo "${P}" | grep -q "${SKIPPED_TESTS_GREP_ARGS}"; then
echo "Skipped ${P}"
continue
Expand Down
23 changes: 13 additions & 10 deletions bin/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
# Init script downloads or updates envoy and the go dependencies. Called from Makefile, which sets
# the needed environment variables.

ROOTDIR=$(cd "$(dirname "$0")"/..; pwd)

set -o errexit
set -o nounset
set -o pipefail
Expand Down Expand Up @@ -92,7 +90,7 @@ function download_envoy_if_necessary () {

# Download and extract the binary to the output directory.
echo "Downloading Envoy: ${DOWNLOAD_COMMAND} $1 to $2"
time ${DOWNLOAD_COMMAND} "$1" | tar xz
time ${DOWNLOAD_COMMAND} --header "${AUTH_HEADER:-}" "$1" | tar xz

# Copy the extracted binary to the output location
cp usr/local/bin/envoy "$2"
Expand All @@ -117,11 +115,14 @@ if [[ -z "${PROXY_REPO_SHA:-}" ]] ; then
export PROXY_REPO_SHA
fi

# Defines the base URL to download envoy from
ISTIO_ENVOY_BASE_URL=${ISTIO_ENVOY_BASE_URL:-https://storage.googleapis.com/istio-build/proxy}

# These variables are normally set by the Makefile.
# OS-neutral vars. These currently only work for linux.
ISTIO_ENVOY_VERSION=${ISTIO_ENVOY_VERSION:-${PROXY_REPO_SHA}}
ISTIO_ENVOY_DEBUG_URL=${ISTIO_ENVOY_DEBUG_URL:-https://storage.googleapis.com/istio-build/proxy/envoy-debug-${ISTIO_ENVOY_LINUX_VERSION}.tar.gz}
ISTIO_ENVOY_RELEASE_URL=${ISTIO_ENVOY_RELEASE_URL:-https://storage.googleapis.com/istio-build/proxy/envoy-alpha-${ISTIO_ENVOY_LINUX_VERSION}.tar.gz}
ISTIO_ENVOY_DEBUG_URL=${ISTIO_ENVOY_DEBUG_URL:-${ISTIO_ENVOY_BASE_URL}/envoy-debug-${ISTIO_ENVOY_LINUX_VERSION}.tar.gz}
ISTIO_ENVOY_RELEASE_URL=${ISTIO_ENVOY_RELEASE_URL:-${ISTIO_ENVOY_BASE_URL}/envoy-alpha-${ISTIO_ENVOY_LINUX_VERSION}.tar.gz}

# Envoy Linux vars. Normally set by the Makefile.
ISTIO_ENVOY_LINUX_VERSION=${ISTIO_ENVOY_LINUX_VERSION:-${ISTIO_ENVOY_VERSION}}
Expand Down Expand Up @@ -177,8 +178,12 @@ mkdir -p "${ISTIO_BIN}"
# Set the value of DOWNLOAD_COMMAND (either curl or wget)
set_download_command

# Download and extract the Envoy linux debug binary.
download_envoy_if_necessary "${ISTIO_ENVOY_LINUX_DEBUG_URL}" "$ISTIO_ENVOY_LINUX_DEBUG_PATH"
if [[ -n "${DEBUG_IMAGE:-}" ]]; then
# Download and extract the Envoy linux debug binary.
download_envoy_if_necessary "${ISTIO_ENVOY_LINUX_DEBUG_URL}" "$ISTIO_ENVOY_LINUX_DEBUG_PATH"
else
echo "Skipping envoy debug. Set DEBUG_IMAGE to download."
fi

# Download and extract the Envoy linux release binary.
download_envoy_if_necessary "${ISTIO_ENVOY_LINUX_RELEASE_URL}" "$ISTIO_ENVOY_LINUX_RELEASE_PATH"
Expand All @@ -188,7 +193,7 @@ if [[ "$LOCAL_OS" == "Darwin" ]]; then
download_envoy_if_necessary "${ISTIO_ENVOY_MACOS_RELEASE_URL}" "$ISTIO_ENVOY_MACOS_RELEASE_PATH"
ISTIO_ENVOY_NATIVE_PATH=${ISTIO_ENVOY_MACOS_RELEASE_PATH}
else
ISTIO_ENVOY_NATIVE_PATH=${ISTIO_ENVOY_LINUX_DEBUG_PATH}
ISTIO_ENVOY_NATIVE_PATH=${ISTIO_ENVOY_LINUX_RELEASE_PATH}
fi

# Copy native envoy binary to ISTIO_OUT
Expand All @@ -200,5 +205,3 @@ cp -f "${ISTIO_ENVOY_NATIVE_PATH}" "${ISTIO_OUT}/envoy"
# Make sure the envoy binary exists. This is only used for tests, so use the debug binary.
echo "Copying ${ISTIO_OUT}/envoy to ${ISTIO_BIN}/envoy"
cp -f "${ISTIO_OUT}/envoy" "${ISTIO_BIN}/envoy"

"${ROOTDIR}/bin/init_helm.sh"
87 changes: 0 additions & 87 deletions bin/init_helm.sh

This file was deleted.

11 changes: 10 additions & 1 deletion bin/protoc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ if [[ $# -le 0 ]]; then
exit 1
fi

RETRY_COUNT=3

api=$(go list -m -f "{{.Dir}}" istio.io/api)

protoc -I"${REPO_ROOT}"/common-protos -I"${api}" "$@"
# This occasionally flakes out, so have a simple retry loop
for (( i=1; i <= RETRY_COUNT; i++ )); do
protoc -I"${REPO_ROOT}"/common-protos -I"${api}" "$@" && break

ret=$?
echo "Attempt ${i}/${RETRY_COUNT} to run protoc failed with exit code ${ret}" >&2
(( i == RETRY_COUNT )) && exit $ret
done
342 changes: 0 additions & 342 deletions bin/testEnvLocalK8S.sh

This file was deleted.

47 changes: 47 additions & 0 deletions bin/update_crds.sh
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"
49 changes: 49 additions & 0 deletions bin/update_deps.sh
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
2 changes: 0 additions & 2 deletions codecov.skip
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ istio.io/istio/pkg/mcp/configz/server/assets
istio.io/istio/pkg/mcp/testing
istio.io/istio/pkg/test
istio.io/istio/samples
istio.io/istio/security/tests/integration
istio.io/istio/tests/codecov
istio.io/istio/tests/e2e
istio.io/istio/tests/binary
istio.io/istio/tests/integration2/examples
istio.io/istio/tests/integration2/qualification
istio.io/istio/tests/integration_old
istio.io/istio/tests/local
istio.io/istio/tests/util
istio.io/istio/tools/githubContrib
Expand Down
2 changes: 1 addition & 1 deletion common/.commonfiles.sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b7abe85fe19c2bd8f66c4903a2624a60b79dac96
ca3ba53a54beac2f6830831b9477c199671bc1b6
35 changes: 25 additions & 10 deletions common/Makefile.common.mk
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FINDFILES=find . \( -path ./common-protos -o -path ./.git -o -path ./.github \) -prune -o -type f
FINDFILES=find . \( -path ./common-protos -o -path ./.git -o -path ./.github -o -path ./licenses \) -prune -o -type f
XARGS = xargs -0 -r

lint-dockerfiles:
Expand All @@ -46,6 +46,8 @@ lint-python:

lint-markdown:
@${FINDFILES} -name '*.md' -print0 | ${XARGS} mdl --ignore-front-matter --style common/config/mdl.rb

lint-links:
@${FINDFILES} -name '*.md' -print0 | ${XARGS} awesome_bot --skip-save-results --allow_ssl --allow-timeout --allow-dupe --allow-redirect --white-list ${MARKDOWN_LINT_WHITELIST}

lint-sass:
Expand All @@ -63,7 +65,10 @@ lint-licenses:

lint-all: lint-dockerfiles lint-scripts lint-yaml lint-helm lint-copyright-banner lint-go lint-python lint-markdown lint-sass lint-typescript lint-protos lint-licenses

format-go:
tidy-go:
@go mod tidy

format-go: tidy-go
@${FINDFILES} -name '*.go' \( ! \( -name '*.gen.go' -o -name '*.pb.go' \) \) -print0 | ${XARGS} goimports -w -local "istio.io"

format-python:
Expand All @@ -80,19 +85,29 @@ dump-licenses-csv:
@go mod download
@license-lint --config common/config/license-lint.yml --csv

mirror-licenses:
@go mod download
@rm -fr licenses
@license-lint --mirror

TMP := $(shell mktemp -d -u)
UPDATE_BRANCH ?= "master"

update-common:
@git clone -q --depth 1 --single-branch --branch master https://github.com/istio/common-files
@cd common-files ; git rev-parse HEAD >files/common/.commonfiles.sha
@mkdir -p $(TMP)
@git clone -q --depth 1 --single-branch --branch $(UPDATE_BRANCH) https://github.com/istio/common-files $(TMP)/common-files
@cd $(TMP)/common-files ; git rev-parse HEAD >files/common/.commonfiles.sha
@rm -fr common
@cp -rT common-files/files .
@rm -fr common-files
@cp -a $(TMP)/common-files/files/* $(shell pwd)
@rm -fr $(TMP)/common-files

update-common-protos:
@git clone -q --depth 1 --single-branch --branch master https://github.com/istio/common-files
@cd common-files ; git rev-parse HEAD > common-protos/.commonfiles.sha
@mkdir -p $(TMP)
@git clone -q --depth 1 --single-branch --branch $(UPDATE_BRANCH) https://github.com/istio/common-files $(TMP)/common-files
@cd $(TMP)/common-files ; git rev-parse HEAD > common-protos/.commonfiles.sha
@rm -fr common-protos
@cp -ar common-files/common-protos common-protos
@rm -fr common-files
@cp -a $(TMP)/common-files/common-protos $(shell pwd)
@rm -fr $(TMP)/common-files

check-clean-repo:
@common/scripts/check_clean_repo.sh
Expand Down
9 changes: 6 additions & 3 deletions common/config/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

service:
# When updating this, also update the version stored in docker/build-tools/Dockerfile in the istio/tools repo.
golangci-lint-version: 1.18.x # use the fixed version to not introduce new linters unexpectedly
golangci-lint-version: 1.21.x # use the fixed version to not introduce new linters unexpectedly
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 20m
Expand All @@ -32,18 +32,21 @@ run:
linters:
enable-all: true
disable:
- bodyclose
- depguard
- dogsled
- dupl
- funlen
- gochecknoglobals
- gochecknoinits
- goconst
- gocyclo
- godox
- gosec
- nakedret
- prealloc
- scopelint
- funlen
- bodyclose
- whitespace
fast: false

linters-settings:
Expand Down
9 changes: 4 additions & 5 deletions common/config/license-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ whitelisted_modules:
- github.com/daviddengcn/go-colortext
- github.com/dchest/siphash
- github.com/dnaeon/go-vcr
- github.com/docker/docker
- github.com/duosecurity/duo_api_golang
- github.com/dustin/go-humanize
- github.com/facebookgo/stack
Expand All @@ -92,9 +91,6 @@ whitelisted_modules:
- github.com/JeffAshton/win_pdh
- github.com/jmespath/go-jmespath
- github.com/jteeuwen/go-bindata
- github.com/juju/errors
- github.com/juju/loggo
- github.com/juju/testing
- github.com/julienschmidt/httprouter
- github.com/koneu/natend
- github.com/kr/logfmt
Expand All @@ -118,7 +114,6 @@ whitelisted_modules:
- github.com/russross/blackfriday
- github.com/russross/blackfriday/v2
- github.com/sean-/seed
- github.com/signalfx/com_signalfx_metrics_protobuf
- github.com/smartystreets/assertions
- github.com/smartystreets/goconvey
- github.com/storageos/go-api
Expand All @@ -127,6 +122,10 @@ whitelisted_modules:
- github.com/xeipuuv/gojsonpointer
- github.com/xeipuuv/gojsonreference
- github.com/xi2/xz

# The core library is MIT licensed, but a submodule imports a BSD-3 license that licensee fails to correctly identify
- github.com/vektah/gqlparser

- github.com/ziutek/mymysql
- gopkg.in/check.v1
- gopkg.in/mgo.v2
Expand Down
7 changes: 4 additions & 3 deletions common/scripts/check_clean_repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
# limitations under the License.

if [[ -n $(git status --porcelain) ]]; then
git status
echo "ERROR: Some files need to be updated, please run make and include any changed files in your PR"
exit 1
git status
git diff
echo "ERROR: Some files need to be updated, please run 'make gen' and include any changed files in your PR"
exit 1
fi
2 changes: 1 addition & 1 deletion common/scripts/lint_go.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

GOGC=25 golangci-lint run -c ./common/config/.golangci.yml
golangci-lint run -c ./common/config/.golangci.yml
2 changes: 2 additions & 0 deletions common/scripts/report_build_info.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ if [[ -n ${ISTIO_VERSION} ]]; then
fi

GIT_DESCRIBE_TAG=$(git describe --tags)
HUB=${HUB:-"docker.io/istio"}

# used by common/scripts/gobuild.sh
echo "istio.io/pkg/version.buildVersion=${VERSION}"
echo "istio.io/pkg/version.buildGitRevision=${BUILD_GIT_REVISION}"
echo "istio.io/pkg/version.buildStatus=${tree_status}"
echo "istio.io/pkg/version.buildTag=${GIT_DESCRIBE_TAG}"
echo "istio.io/pkg/version.buildHub=${HUB}"
39 changes: 0 additions & 39 deletions docker/Dockerfile.bionic_debug

This file was deleted.

38 changes: 0 additions & 38 deletions docker/Dockerfile.deb_debug

This file was deleted.

21 changes: 21 additions & 0 deletions docker/Dockerfile.istiod
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"]
28 changes: 0 additions & 28 deletions docker/Dockerfile.xenial_debug

This file was deleted.

10 changes: 5 additions & 5 deletions galley/cmd/galley/cmd/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ func probeCmd() *cobra.Command {
prb := &cobra.Command{
Use: "probe",
Short: "Check the liveness or readiness of a locally-running server",
Run: func(cmd *cobra.Command, _ []string) {
RunE: func(cmd *cobra.Command, _ []string) error {
if !probeOptions.IsValid() {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "some options are not valid")
return
return fmt.Errorf("some options are not valid")
}
if err := probe.NewFileClient(&probeOptions).GetStatus(); err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "fail on inspecting path %s: %v", probeOptions.Path, err)
return fmt.Errorf("fail on inspecting path %s: %v", probeOptions.Path, err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "OK")
fmt.Fprintf(cmd.OutOrStdout(), "OK")
return nil
},
}
prb.PersistentFlags().StringVar(&probeOptions.Path, "probe-path", "",
Expand Down
13 changes: 8 additions & 5 deletions galley/cmd/galley/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func serverCmd() *cobra.Command {
"Use insecure gRPC communication")
svr.PersistentFlags().BoolVar(&serverArgs.EnableServer, "enable-server", serverArgs.EnableServer, "Run galley server mode")
svr.PersistentFlags().StringVarP(&serverArgs.AccessListFile, "accessListFile", "", serverArgs.AccessListFile,
"The access list yaml file that contains the allowd mTLS peer ids.")
"The access list yaml file that contains the allowed mTLS peer ids.")
svr.PersistentFlags().StringVar(&serverArgs.ConfigPath, "configPath", serverArgs.ConfigPath,
"Istio config file path")
svr.PersistentFlags().StringVar(&serverArgs.MeshConfigFile, "meshConfigFile", serverArgs.MeshConfigFile,
Expand All @@ -153,17 +153,20 @@ func serverCmd() *cobra.Command {
serverArgs.SinkMeta, "Comma-separated list of key=values to attach as metadata to outgoing sink connections. Ex: 'key=value,key2=value2'")
svr.PersistentFlags().BoolVar(&serverArgs.EnableServiceDiscovery, "enableServiceDiscovery", false,
"Enable service discovery processing in Galley")
svr.PersistentFlags().BoolVar(&serverArgs.UseOldProcessor, "useOldProcessor", serverArgs.UseOldProcessor,
"Use the old processing pipeline for config processing")
_ = svr.PersistentFlags().Bool("useOldProcessor", false, "Use the old processing pipeline for config processing")
_ = svr.PersistentFlags().MarkDeprecated("useOldProcessor",
"--useOldProcessor is deprecated and has no effect. The new pipeline is the only pipeline")
svr.PersistentFlags().BoolVar(&serverArgs.WatchConfigFiles, "watchConfigFiles", serverArgs.WatchConfigFiles,
"Enable the Fsnotify for watching config source files on the disk and implicit signaling on a config change. Explicit signaling will still be enabled")
svr.PersistentFlags().BoolVar(&serverArgs.EnableConfigAnalysis, "enableAnalysis", serverArgs.EnableConfigAnalysis,
"Enable config analysis service")

// validation config
svr.PersistentFlags().StringVar(&serverArgs.ValidationArgs.WebhookConfigFile,
"validation-webhook-config-file", "",
"File that contains k8s validatingwebhookconfiguration yaml. Required if enable-validation is true.")
svr.PersistentFlags().UintVar(&serverArgs.ValidationArgs.Port, "validation-port", 443,
"HTTPS port of the validation service. Must be 443 if service has more than one port ")
svr.PersistentFlags().UintVar(&serverArgs.ValidationArgs.Port, "validation-port",
serverArgs.ValidationArgs.Port, "HTTPS port of the validation service.")
svr.PersistentFlags().BoolVar(&serverArgs.ValidationArgs.EnableValidation, "enable-validation", serverArgs.ValidationArgs.EnableValidation,
"Run galley validation mode")
svr.PersistentFlags().BoolVar(&serverArgs.ValidationArgs.EnableReconcileWebhookConfiguration,
Expand Down
4 changes: 3 additions & 1 deletion galley/docker/Dockerfile.galley
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ 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:latest as distroless
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
Expand Down
15 changes: 9 additions & 6 deletions galley/pkg/config/analysis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ func (s *GatewayAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
// Each analyzer should have a unique name. Use <top-level-pkg>.<struct type>
Name: "virtualservice.GatewayAnalyzer",

// Each analyzer should have a short, one line description of what they
// do. This description is shown when --list-analyzers is called via
// the command line.
Description: "Checks that VirtualService resources reference Gateways that exist"
// Each analyzer should register the collections that it needs to use as input.
Inputs: collection.Names{
metadata.IstioNetworkingV1Alpha3Gateways,
Expand Down Expand Up @@ -127,9 +130,9 @@ e.g. for the GatewayAnalyzer used as an example above, you would add something l
},
// A single specific analyzer to run
analyzer: &virtualservice.GatewayAnalyzer{},
// List of expected validation messages, as (messageType, <kind>/<name>) tuples
// List of expected validation messages, as (messageType, <kind> <name>[.<namespace>]) tuples
expected: []message{
{msg.ReferencedResourceNotFound, "VirtualService/httpbin-bogus"},
{msg.ReferencedResourceNotFound, "VirtualService httpbin-bogus"},
},
},
```
Expand Down Expand Up @@ -177,16 +180,16 @@ inputs in the analyzer metadata. This should help you find any unused inputs and

### 5. Testing via istioctl

You can use `istioctl experimental analyze` to run all analyzers, including your new one. e.g.
You can use `istioctl analyze` to run all analyzers, including your new one. e.g.

```sh
make istioctl && $GOPATH/out/linux_amd64/release/istioctl experimental analyze
make istioctl && $GOPATH/out/linux_amd64/release/istioctl analyze
```

### 6. Write a user-facing documentation page

Each analysis message needs to be documented for customers. This is done by introducing a markdown file for
each message in the istio.io repo in the content/en/docs/reference/config/analysis directory. You create
each message in the [istio.io](https://github.com/istio/istio.io) repo in the [content/en/docs/reference/config/analysis](https://github.com/istio/istio.io/tree/master/content/en/docs/reference/config/analysis) directory. You create
a subdirectory with the code of the error message, and add a `index.md` file that contains the
full description of the problem with potential remediation steps, examples, etc. See the existing
files in that directory for examples of how this is done.
Expand Down
82 changes: 55 additions & 27 deletions galley/pkg/config/analysis/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,30 @@ type Analyzer interface {

// CombinedAnalyzer is a special Analyzer that combines multiple analyzers into one
type CombinedAnalyzer struct {
metadata Metadata
name string
analyzers []Analyzer
disabled map[collection.Name]struct{}
}

// Combine multiple analyzers into a single one.
// For input metadata, use the union of the component analyzers
func Combine(name string, analyzers ...Analyzer) *CombinedAnalyzer {
return &CombinedAnalyzer{
metadata: Metadata{
Name: name,
Inputs: combineInputs(analyzers),
},
name: name,
analyzers: analyzers,
}
}

// Metadata implements Analyzer
func (c *CombinedAnalyzer) Metadata() Metadata {
return c.metadata
return Metadata{
Name: c.name,
Inputs: combineInputs(c.analyzers),
}
}

// Analyze implements Analyzer
func (c *CombinedAnalyzer) Analyze(ctx Context) {
mainloop:
for _, a := range c.analyzers {
// Skip over any analyzers that require disabled input
for _, in := range a.Metadata().Inputs {
if _, ok := c.disabled[in]; ok {
scope.Analysis.Debugf("Skipping analyzer %q because collection %s is disabled.", a.Metadata().Name, in)
continue mainloop
}
}

scope.Analysis.Debugf("Started analyzer %q...", a.Metadata().Name)
if ctx.Canceled() {
scope.Analysis.Debugf("Analyzer %q has been cancelled...", c.Metadata().Name)
Expand All @@ -72,6 +62,55 @@ mainloop:
}
}

// RemoveSkipped removes analyzers that should be skipped, meaning they meet one of the following criteria:
// 1. The analyzer requires disabled input collections. The names of removed analyzers are returned.
// Transformer information is used to determine, based on the disabled input collections, which output collections
// should be disabled. Any analyzers that require those output collections will be removed.
// 2. The analyzer requires a collection not available in the current snapshot(s)
func (c *CombinedAnalyzer) RemoveSkipped(colsInSnapshots, disabledInputs collection.Names, xformProviders transformer.Providers) []string {
disabledOutputs := getDisabledOutputs(disabledInputs, xformProviders)
var enabled []Analyzer
var removedNames []string

snapshotCols := make(map[collection.Name]bool)
for _, col := range colsInSnapshots {
snapshotCols[col] = true
}

mainloop:
for _, a := range c.analyzers {
for _, in := range a.Metadata().Inputs {
// Skip over any analyzers that require disabled input
if _, ok := disabledOutputs[in]; ok {
scope.Analysis.Infof("Skipping analyzer %q because collection %s is disabled.", a.Metadata().Name, in)
removedNames = append(removedNames, a.Metadata().Name)
continue mainloop
}

// Skip over any analyzers needing collections not in the snapshot(s)
if _, ok := snapshotCols[in]; !ok {
scope.Analysis.Infof("Skipping analyzer %q because collection %s is not in the snapshot(s).", a.Metadata().Name, in)
removedNames = append(removedNames, a.Metadata().Name)
continue mainloop
}
}

enabled = append(enabled, a)
}

c.analyzers = enabled
return removedNames
}

// AnalyzerNames returns the names of analyzers in this combined analyzer
func (c *CombinedAnalyzer) AnalyzerNames() []string {
var result []string
for _, a := range c.analyzers {
result = append(result, a.Metadata().Name)
}
return result
}

func combineInputs(analyzers []Analyzer) collection.Names {
result := make([]collection.Name, 0)
for _, a := range analyzers {
Expand All @@ -81,17 +120,6 @@ func combineInputs(analyzers []Analyzer) collection.Names {
return result
}

// WithDisabled returns a new CombinedAnalyzer that marks the specified input collections as disabled.
// Transformer information is used to determine, based on the disabled input collections, which output collections
// should be disabled. Any analyzers that require those output collections will be skipped.
func (c *CombinedAnalyzer) WithDisabled(disabledInputs collection.Names, xformProviders transformer.Providers) *CombinedAnalyzer {
return &CombinedAnalyzer{
metadata: c.metadata,
analyzers: c.analyzers,
disabled: getDisabledOutputs(disabledInputs, xformProviders),
}
}

func getDisabledOutputs(disabledInputs collection.Names, xformProviders transformer.Providers) map[collection.Name]struct{} {
// Get disabledCollections as a set
disabledInputSet := make(map[collection.Name]struct{})
Expand Down
30 changes: 22 additions & 8 deletions galley/pkg/config/analysis/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ import (
"istio.io/istio/galley/pkg/config/processing"
"istio.io/istio/galley/pkg/config/processing/transformer"
"istio.io/istio/galley/pkg/config/resource"
"istio.io/istio/galley/pkg/config/testing/data"
)

type analyzer struct {
name string
inputs collection.Names
ran bool
}

// Metadata implements Analyzer
func (a *analyzer) Metadata() Metadata {
return Metadata{
Name: "",
Name: a.name,
Inputs: a.inputs,
}
}
Expand All @@ -57,21 +57,35 @@ func (ctx *context) Canceled() bool
func TestCombinedAnalyzer(t *testing.T) {
g := NewGomegaWithT(t)

a1 := &analyzer{inputs: collection.Names{data.Collection1}}
a2 := &analyzer{inputs: collection.Names{data.Collection2}}
a3 := &analyzer{inputs: collection.Names{data.Collection3}}
col1 := collection.NewName("col1")
col2 := collection.NewName("col2")
col3 := collection.NewName("col3")
col4 := collection.NewName("col4")

xform := transformer.NewSimpleTransformerProvider(data.Collection3, data.Collection3, func(_ event.Event, _ event.Handler) {})
a1 := &analyzer{name: "a1", inputs: collection.Names{col1}}
a2 := &analyzer{name: "a2", inputs: collection.Names{col2}}
a3 := &analyzer{name: "a3", inputs: collection.Names{col3}}
a4 := &analyzer{name: "a4", inputs: collection.Names{col4}}

a := Combine("combined", a1, a2, a3).WithDisabled(collection.Names{data.Collection3}, transformer.Providers{xform})
xform := transformer.NewSimpleTransformerProvider(col3, col3, func(_ event.Event, _ event.Handler) {})

g.Expect(a.Metadata().Inputs).To(ConsistOf(data.Collection1, data.Collection2, data.Collection3))
a := Combine("combined", a1, a2, a3, a4)
g.Expect(a.Metadata().Inputs).To(ConsistOf(col1, col2, col3, col4))

removed := a.RemoveSkipped(
collection.Names{col1, col2, col3},
collection.Names{col3},
transformer.Providers{xform})

g.Expect(removed).To(ConsistOf(a3.Metadata().Name, a4.Metadata().Name))
g.Expect(a.Metadata().Inputs).To(ConsistOf(col1, col2))

a.Analyze(&context{})

g.Expect(a1.ran).To(BeTrue())
g.Expect(a2.ran).To(BeTrue())
g.Expect(a3.ran).To(BeFalse())
g.Expect(a4.ran).To(BeFalse())
}

func TestGetDisabledOutputs(t *testing.T) {
Expand Down
11 changes: 11 additions & 0 deletions galley/pkg/config/analysis/analyzers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import (
"istio.io/istio/galley/pkg/config/analysis"
"istio.io/istio/galley/pkg/config/analysis/analyzers/annotations"
"istio.io/istio/galley/pkg/config/analysis/analyzers/auth"
"istio.io/istio/galley/pkg/config/analysis/analyzers/deployment"
"istio.io/istio/galley/pkg/config/analysis/analyzers/deprecation"
"istio.io/istio/galley/pkg/config/analysis/analyzers/gateway"
"istio.io/istio/galley/pkg/config/analysis/analyzers/injection"
"istio.io/istio/galley/pkg/config/analysis/analyzers/schema"
"istio.io/istio/galley/pkg/config/analysis/analyzers/service"
"istio.io/istio/galley/pkg/config/analysis/analyzers/sidecar"
"istio.io/istio/galley/pkg/config/analysis/analyzers/virtualservice"
)

Expand All @@ -30,11 +33,19 @@ func All() []analysis.Analyzer {
analyzers := []analysis.Analyzer{
// Please keep this list sorted alphabetically by pkg.name for convenience
&annotations.K8sAnalyzer{},
&auth.MTLSAnalyzer{},
&auth.ServiceRoleBindingAnalyzer{},
&auth.ServiceRoleServicesAnalyzer{},
&deployment.ServiceAssociationAnalyzer{},
&deprecation.FieldAnalyzer{},
&gateway.IngressGatewayPortAnalyzer{},
&gateway.SecretAnalyzer{},
&injection.Analyzer{},
&injection.VersionAnalyzer{},
&service.PortNameAnalyzer{},
&sidecar.DefaultSelectorAnalyzer{},
&sidecar.SelectorAnalyzer{},
&virtualservice.ConflictingMeshGatewayHostsAnalyzer{},
&virtualservice.DestinationHostAnalyzer{},
&virtualservice.DestinationRuleAnalyzer{},
&virtualservice.GatewayAnalyzer{},
Expand Down
302 changes: 265 additions & 37 deletions galley/pkg/config/analysis/analyzers/analyzers_test.go

Large diffs are not rendered by default.

143 changes: 41 additions & 102 deletions galley/pkg/config/analysis/analyzers/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,108 +26,39 @@ import (
"istio.io/istio/galley/pkg/config/resource"
)

// K8sAnalyzer checks for misplayed and invalid Istio annotations in K8s resources
// K8sAnalyzer checks for misplaced and invalid Istio annotations in K8s resources
type K8sAnalyzer struct{}

var (
// Currently we don't have an Istio API that enumerates Istio annotations.
// This slice is currently hand-crafted, but
// could be generated similarly to https://istio.io/docs/reference/config/annotations/
istioAnnotations = []*annotation.Instance{
&annotation.AlphaCanonicalServiceAccounts,
&annotation.AlphaIdentity,
&annotation.AlphaKubernetesServiceAccounts,
&annotation.IoKubernetesIngressClass,
&annotation.AlphaNetworkingEndpointsVersion,
&annotation.AlphaNetworkingNotReadyEndpoints,
&annotation.AlphaNetworkingServiceVersion,
&annotation.NetworkingExportTo,
&annotation.PolicyCheck,
&annotation.PolicyCheckBaseRetryWaitTime,
&annotation.PolicyCheckMaxRetryWaitTime,
&annotation.PolicyCheckRetries,
&annotation.PolicyLang,
&annotation.SidecarStatusReadinessApplicationPorts,
&annotation.SidecarStatusReadinessFailureThreshold,
&annotation.SidecarStatusReadinessInitialDelaySeconds,
&annotation.SidecarStatusReadinessPeriodSeconds,
&annotation.SecurityAutoMTLS,
&annotation.SidecarBootstrapOverride,
&annotation.SidecarComponentLogLevel,
&annotation.SidecarControlPlaneAuthPolicy,
&annotation.SidecarDiscoveryAddress,
&annotation.SidecarInject,
&annotation.SidecarInterceptionMode,
&annotation.SidecarLogLevel,
&annotation.SidecarProxyCPU,
&annotation.SidecarProxyImage,
&annotation.SidecarProxyMemory,
&annotation.SidecarRewriteAppHTTPProbers,
&annotation.SidecarStatsInclusionPrefixes,
&annotation.SidecarStatsInclusionRegexps,
&annotation.SidecarStatsInclusionSuffixes,
&annotation.SidecarStatus,
&annotation.SidecarUserVolume,
&annotation.SidecarUserVolumeMount,
&annotation.SidecarStatusPort,
&annotation.SidecarTrafficExcludeInboundPorts,
&annotation.SidecarTrafficExcludeOutboundIPRanges,
&annotation.SidecarTrafficExcludeOutboundPorts,
&annotation.SidecarTrafficIncludeInboundPorts,
&annotation.SidecarTrafficIncludeOutboundIPRanges,
&annotation.SidecarTrafficKubevirtInterfaces,
}

// Currently we don't have an Istio API that enumerates Istio annotations ResourceTypes
// (This map is currently hand-crafted. ResourceTypes could be a []string, not an enum.)
resourceTypeNames = map[annotation.ResourceTypes]string{
annotation.Ingress: "Ingress",
annotation.Pod: "Pod",
annotation.Service: "Service",
annotation.ServiceEntry: "ServiceEntry",
}
istioAnnotations = annotation.AllResourceAnnotations()
)

// Metadata implements analyzer.Analyzer
func (*K8sAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
Name: "annotations.K8sAnalyzer",
Name: "annotations.K8sAnalyzer",
Description: "Checks for misplaced and invalid Istio annotations in Kubernetes resources",
Inputs: collection.Names{
metadata.K8SCoreV1Endpoints,
metadata.K8SCoreV1Namespaces,
metadata.K8SCoreV1Services,
metadata.K8SCoreV1Pods,
metadata.K8SExtensionsV1Beta1Ingresses,
metadata.K8SCoreV1Nodes,
metadata.K8SAppsV1Deployments,
},
}
}

// Analyze implements analysis.Analyzer
func (fa *K8sAnalyzer) Analyze(ctx analysis.Context) {
ctx.ForEach(metadata.K8SCoreV1Endpoints, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Endpoints", metadata.K8SCoreV1Endpoints)
return true
})
ctx.ForEach(metadata.K8SCoreV1Namespaces, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Namespace", metadata.K8SCoreV1Namespaces)
return true
})
ctx.ForEach(metadata.K8SCoreV1Nodes, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Node", metadata.K8SCoreV1Nodes)
return true
})
ctx.ForEach(metadata.K8SCoreV1Pods, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Pod", metadata.K8SCoreV1Pods)
return true
})
ctx.ForEach(metadata.K8SCoreV1Services, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Service", metadata.K8SCoreV1Services)
return true
})
ctx.ForEach(metadata.K8SExtensionsV1Beta1Ingresses, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Ingress", metadata.K8SExtensionsV1Beta1Ingresses)
ctx.ForEach(metadata.K8SCoreV1Pods, func(r *resource.Entry) bool {
fa.allowAnnotations(r, ctx, "Pod", metadata.K8SCoreV1Pods)
return true
})
ctx.ForEach(metadata.K8SAppsV1Deployments, func(r *resource.Entry) bool {
Expand All @@ -142,34 +73,44 @@ func (*K8sAnalyzer) allowAnnotations(r *resource.Entry, ctx analysis.Context, ki
}

// It is fine if the annotation is kubectl.kubernetes.io/last-applied-configuration.
outer:
for ann := range r.Metadata.Annotations {
if istioAnnotation(ann) {
if !istioAnnotation(ann) {
continue
}

annotationDef := lookupAnnotation(ann)
if annotationDef == nil {
ctx.Report(collectionType,
msg.NewUnknownAnnotation(r, ann))
continue
}
annotationDef := lookupAnnotation(ann)
if annotationDef == nil {
ctx.Report(collectionType,
msg.NewUnknownAnnotation(r, ann))
continue
}

attachesTo := resourceTypesAsStrings(annotationDef.Resources)
if !contains(attachesTo, kind) {
ctx.Report(collectionType,
msg.NewMisplacedAnnotation(r, ann, strings.Join(attachesTo, ", ")))
continue
// If the annotation def attaches to Any, exit early
for _, rt := range annotationDef.Resources {
if rt == annotation.Any {
continue outer
}
}

// TODO: Check annotation.Deprecated. Not implemented because no
// deprecations in the table have yet been deprecated!

// Note: currently we don't validate the value of the annotation,
// just key and placement.
// There is annotation value validation (for pod annotations) in the web hook at
// istio.io/istio/pkg/kube/inject/inject.go. Rather than refactoring
// that code I intend to bring all of the web hook checks into this framework
// and checking here will be redundant.
attachesTo := resourceTypesAsStrings(annotationDef.Resources)
if !contains(attachesTo, kind) {
ctx.Report(collectionType,
msg.NewMisplacedAnnotation(r, ann, strings.Join(attachesTo, ", ")))
continue
}

// TODO: Check annotation.Deprecated. Not implemented because no
// deprecations in the table have yet been deprecated!

// Note: currently we don't validate the value of the annotation,
// just key and placement.
// There is annotation value validation (for pod annotations) in the web hook at
// istio.io/istio/pkg/kube/inject/inject.go. Rather than refactoring
// that code I intend to bring all of the web hook checks into this framework
// and checking here will be redundant.
}

}

// istioAnnotation is true if the annotation is in Istio's namespace
Expand Down Expand Up @@ -212,12 +153,10 @@ func lookupAnnotation(ann string) *annotation.Instance {
}

func resourceTypesAsStrings(resourceTypes []annotation.ResourceTypes) []string {
retval := make([]string, len(resourceTypes))
var ok bool
for i, resourceType := range resourceTypes {
retval[i], ok = resourceTypeNames[resourceType]
if !ok {
retval[i] = "Unknown"
retval := []string{}
for _, resourceType := range resourceTypes {
if s := resourceType.String(); s != "Unknown" {
retval = append(retval, s)
}
}
return retval
Expand Down
369 changes: 369 additions & 0 deletions galley/pkg/config/analysis/analyzers/auth/mtls.go
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
}
289 changes: 289 additions & 0 deletions galley/pkg/config/analysis/analyzers/auth/mtls/policy_checker.go
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
}
446 changes: 446 additions & 0 deletions galley/pkg/config/analysis/analyzers/auth/mtls/policy_checker_test.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ var _ analysis.Analyzer = &ServiceRoleBindingAnalyzer{}
// Metadata implements Analyzer
func (s *ServiceRoleBindingAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
Name: "auth.ServiceRoleBindingAnalyzer",
Name: "auth.ServiceRoleBindingAnalyzer",
Description: "Checks the validity of service role bindings",
Inputs: collection.Names{
metadata.IstioRbacV1Alpha1Serviceroles,
metadata.IstioRbacV1Alpha1Servicerolebindings,
Expand Down
97 changes: 97 additions & 0 deletions galley/pkg/config/analysis/analyzers/auth/serviceroleservices.go
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)
}
180 changes: 180 additions & 0 deletions galley/pkg/config/analysis/analyzers/deployment/services.go
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ type FieldAnalyzer struct{}
// Metadata implements analyzer.Analyzer
func (*FieldAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
Name: "deprecation.DeprecationAnalyzer",
Name: "deprecation.DeprecationAnalyzer",
Description: "Checks for deprecated Istio types and fields",
Inputs: collection.Names{
metadata.IstioNetworkingV1Alpha3Virtualservices,
metadata.IstioNetworkingV1Alpha3Envoyfilters,
Expand Down Expand Up @@ -165,7 +166,7 @@ func replacedMessage(deprecated, replacement string) string {
}

// uncertainFixMessage() should be used for fields we don't have a suggested replacement for.
// It is preferrable to avoid calling it and find out the replacement suggestion instead.
// It is preferable to avoid calling it and find out the replacement suggestion instead.
func uncertainFixMessage(field string) string {
return fmt.Sprintf("%s is deprecated", field)
}
6 changes: 4 additions & 2 deletions galley/pkg/config/analysis/analyzers/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"istio.io/istio/galley/pkg/config/resource"
)

// IngressGatewayPortAnalyzer checks a Gateway's ports against the gateway's K8s Service ports.
// IngressGatewayPortAnalyzer checks a gateway's ports against the gateway's Kubernetes service ports.
type IngressGatewayPortAnalyzer struct{}

var (
Expand All @@ -48,7 +48,8 @@ var (
// Metadata implements analysis.Analyzer
func (*IngressGatewayPortAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
Name: "gateway.IngressGatewayPortAnalyzer",
Name: "gateway.IngressGatewayPortAnalyzer",
Description: "Checks a gateway's ports against the gateway's Kubernetes service ports",
Inputs: collection.Names{
metadata.IstioNetworkingV1Alpha3Gateways,
metadata.K8SCoreV1Pods,
Expand Down Expand Up @@ -110,6 +111,7 @@ func (*IngressGatewayPortAnalyzer) analyzeGateway(r *resource.Entry, c analysis.
// the Istio system ingress gateway complain about a missing referenced resource. (We
// don't want to complain about missing system resources, because a user may want to analyze
// only his own application files.)
// https://github.com/istio/istio/issues/19579 should make this unnecessary
if len(gw.Selector) != 1 || gw.Selector["istio"] != "ingressgateway" {
c.Report(metadata.IstioNetworkingV1Alpha3Gateways, msg.NewReferencedResourceNotFound(r, "selector", gwSelector.String()))
return
Expand Down
Loading