pkg/mesh/istio.go (386 lines of code) (raw):

// Copyright 2021 Google LLC // // 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 // // https://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 mesh import ( "bytes" "fmt" "io" "io/ioutil" "log" "os" "os/exec" "strconv" "strings" "syscall" "time" "github.com/creack/pty" ) // Istio injected environment: // // - env variables - we load them form 'mesh-env' plus internal // // - volumes: // /var/run/secrets/istio - istiod-ca-cert - confingMap:istio-ca-root-cert // /var/lib/istio/data - istio-data - empty dir ??? // /etc/istio/proxy - istio-envoy - memory, RW // /var/run/secrets/tokens - istio-token - audience=trustDomain // /etc/istio/pod - istio-podinfo - labels, annotations // /var/run/secrets/kubernetes.io/serviceaccount - xx-token-yy (by kubelet/service account controller) // // MeshConfig is a minimal mesh config - used to load in-cluster settings used in injection. type MeshConfig struct { TrustDomain string `yaml:"trustDomain,omitempty"` DefaultConfig ProxyConfig `yaml:"defaultConfig,omitempty"` } type ProxyConfig struct { DiscoveryAddress string `yaml:"discoveryAddress,omitempty"` MeshId string `yaml:"meshId,omitempty"` ProxyMetadata map[string]string `yaml:"proxyMetadata,omitempty"` CaCertificatesPem []string `yaml:"caCertificatesPem,omitempty"` } // Setup /etc/resolv.conf when running as root, with pilot-agent resolving DNS // // When running as root: // - if /var/lib/istio/resolv.conf is found, use it. // - else, copy /etc/resolv.conf to /var/lib/istio/resolv.conf and create a new resolv.conf func resolvConfForRoot() { if _, err := os.Stat("/var/lib/istio/resolv.conf"); !os.IsNotExist(err) { log.Println("Alternate resolv.conf exists") return } os.MkdirAll("/var/lib/istio", 0755) data, err := os.ReadFile("/etc/resolv.conf") if err != nil { log.Println("Failed to read resolv.conf, DNS interception will fail ", err) return } err = os.WriteFile("/var/lib/istio/resolv.conf", data, 0755) if err != nil { log.Println("Failed to create alternate resolv.conf, DNS interception will fail ", err) return } err = os.WriteFile("/etc/resolv.conf", []byte(`nameserver: 127.0.0.1\nsearch: google.internal.`), 755) if err != nil { log.Println("Failed to create resolv.conf, DNS interception will fail ", err) return } log.Println("Adjusted resolv.conf") } func (kr *KRun) agentCommand() *exec.Cmd { // From the template: //- proxy //- sidecar //- --domain //- $(POD_NAMESPACE).svc.{{ .Values.global.proxy.clusterDomain }} //- --proxyLogLevel={{ annotation .ObjectMeta `sidecar.istio.io/logLevel` .Values.global.proxy.logLevel }} //- --proxyComponentLogLevel={{ annotation .ObjectMeta `sidecar.istio.io/componentLogLevel` .Values.global.proxy.componentLogLevel }} //- --log_output_level={{ annotation .ObjectMeta `sidecar.istio.io/agentLogLevel` .Values.global.logging.level }} //{{- if .Values.global.sts.servicePort }} //- --stsPort={{ .Values.global.sts.servicePort }} //{{- end }} //{{- if .Values.global.logAsJson }} //- --log_as_json //{{- end }} //{{- if gt .EstimatedConcurrency 0 }} //- --concurrency //- "{{ .EstimatedConcurrency }}" //{{- end -}} //{{- if .Values.global.proxy.lifecycle }} args := []string{"proxy"} if kr.Gateway != "" { args = append(args, "router") } else { args = append(args, "sidecar") } args = append(args, "--domain") args = append(args, kr.Namespace+".svc.cluster.local") args = append(args, "--serviceCluster") args = append(args, kr.Name+"."+kr.Namespace) if kr.AgentDebug != "" { args = append(args, "--log_output_level="+kr.AgentDebug) } if os.Getenv("ENVOY_LOG_LEVEL") != "" { args = append(args, "--proxyLogLevel="+os.Getenv("ENVOY_LOG_LEVEL")) } args = append(args, "--stsPort=15463") return exec.Command("/usr/local/bin/pilot-agent", args...) } // StartIstioAgent creates the env and starts istio agent. // If running as root, will also init iptables and change UID to 1337. func (kr *KRun) StartIstioAgent() error { if kr.XDSAddr == "-" { return nil } prefix := "." if os.Getuid() == 0 { prefix = "" } os.MkdirAll(prefix+"/etc/istio/proxy", 0755) //os.MkdirAll(prefix+"/var/lib/istio/envoy", 0755) // Save the istio certificates - for proxyless or app use. os.MkdirAll(prefix+"/var/run/secrets/istio", 0755) os.MkdirAll(prefix+"/var/run/secrets/mesh", 0755) os.MkdirAll(prefix+"/var/run/secrets/istio.io", 0755) os.MkdirAll(prefix+"/etc/istio/pod", 0755) if os.Getuid() == 0 { //os.Chown(prefix+"/var/lib/istio/envoy", 1337, 1337) os.Chown(prefix+"/var/run/secrets/istio.io", 1337, 1337) os.Chown(prefix+"/var/run/secrets/istio", 1337, 1337) os.Chown(prefix+"/var/run/secrets/mesh", 1337, 1337) os.Chown(prefix+"/etc/istio/pod", 1337, 1337) os.Chown(prefix+"/etc/istio/proxy", 1337, 1337) } // Pilot agent expects this file, containing citadel roots. Will be used to connect to the XDS server, and as // default root CA. if kr.CitadelRoot != "" { err := ioutil.WriteFile(prefix+"/var/run/secrets/istio/root-cert.pem", []byte(kr.CitadelRoot), 0755) if err != nil { log.Println("Failed to write citadel root", "rootCAFile", prefix + "/var/run/secrets/istio/root-cert.pem", "error", err) } } // /dev/stdout is rejected - it is a pipe. // https://github.com/envoyproxy/envoy/issues/8297#issuecomment-620659781 if kr.Name == "" && kr.Gateway != "" { kr.Name = kr.Gateway } env := os.Environ() // XDS and CA servers are using system certificates ( recommended ). // If using a private CA - add it's root to the docker images, everything will be consistent // and simpler ! proxyConfigEnv := os.Getenv("PROXY_CONFIG") if proxyConfigEnv == "" { addr := kr.FindXDSAddr() kr.XDSAddr = addr log.Println("XDSAddr discovery", addr, "XDS_ADDR", kr.XDSAddr, "MESH_TENANT", kr.MeshTenant) proxyConfig := fmt.Sprintf(`{"discoveryAddress": "%s"}`, addr) env = append(env, "PROXY_CONFIG="+proxyConfig) } else { log.Println("Using injected PROXY_CONFIG", proxyConfigEnv) } // Pilot-agent requires this file, to connect to CA and XDS. // The plan is to add code to get the certs to this package, so proxyless doesn't depend on pilot-agent. // // OSS Istio uses 'istio-ca' as token audience when connecting to Citadel // ASM uses the 'trust domain' - which is also needed for MCP and Stackdriver. // Recent Istiod supports customization of the expected audiences, via an env variable. // if strings.HasSuffix(kr.XDSAddr, ":15012") { env = addIfMissing(env, "ISTIOD_SAN", "istiod.istio-system.svc") // Temp workaround to handle OSS-specific behavior. By default we will expect OSS Istio // to be installed in 'compatibility' mode with ASM, i.e. accept both istio-ca and trust domain // as audience. // TODO: use the trust domain from mesh-env if os.Getenv("OSS_ISTIO") != "" { log.Println("Using istio-ca audience") kr.Aud2File["istio-ca"] = kr.BaseDir + "/var/run/secrets/tokens/istio-token" } else { log.Println("Using audience", kr.TrustDomain) kr.Aud2File[kr.TrustDomain] = kr.BaseDir + "/var/run/secrets/tokens/istio-token" } } else { log.Println("Using system certifates for XDS and CA") kr.Aud2File[kr.TrustDomain] = kr.BaseDir + "/var/run/secrets/tokens/istio-token" env = addIfMissing(env, "XDS_ROOT_CA", "SYSTEM") env = addIfMissing(env, "PILOT_CERT_PROVIDER", "system") env = addIfMissing(env, "CA_ROOT_CA", "SYSTEM") } env = addIfMissing(env, "POD_NAMESPACE", kr.Namespace) kr.RefreshAndSaveTokens() // Pod name MUST be an unique name - it is used in stackdriver which requires this ( errors on 'ordered updates' and // lost data otherwise) // This also shows up in 'istioctl ps' and in istio logs // K_REVISION (ex: fortio-cr-00011-duq) and metadata. podName := os.Getenv("K_REVISION") hn := os.Getenv("HOSTNAME") if hn == "" { hn, _ = os.Hostname() hnp := strings.Split(hn, ".") if len(hnp) > 0 { hn = hnp[0] } } if podName != "" { if kr.InstanceID == "" { podName = podName + "-" + strconv.Itoa(time.Now().Second()) } else if len(kr.InstanceID) > 8 { podName = podName + "-" + kr.InstanceID[0:8] } else { podName = podName + "-" + kr.InstanceID } if kr.Rev == "" { kr.Rev = podName } } else if hn != "" { podName = os.Getenv("HOSTNAME") } else { podName = kr.Name + "-" + "-" + strconv.Itoa(time.Now().Second()) log.Println("Setting POD_NAME from name, missing instance ", podName) } // Some default value. if kr.Rev == "" { kr.Rev = "v1" } // If running in k8s, this is set to an unique ID env = addIfMissing(env, "POD_NAME", podName) env = addIfMissing(env, "ISTIO_META_WORKLOAD_NAME", kr.Name) env = addIfMissing(env, "SERVICE_ACCOUNT", kr.KSA) if kr.ProjectNumber != "" { env = addIfMissing(env, "ISTIO_META_MESH_ID", "proj-"+kr.ProjectNumber) } env = addIfMissing(env, "CANONICAL_SERVICE", kr.Name) env = addIfMissing(env, "CANONICAL_REVISION", kr.Rev) kr.initLabelsFile() env = addIfMissing(env, "OUTPUT_CERTS", prefix+"/var/run/secrets/istio.io/") // This would be used if a audience-less JWT was present - not possible with TokenRequest // TODO: add support for passing a long lived 1p JWT in a file, for local run //env = append(env, "JWT_POLICY=first-party-jwt") kr.WhiteboxMode = kr.Config("ISTIO_META_INTERCEPTION_MODE", "") == "NONE" if os.Getuid() != 0 { kr.WhiteboxMode = true } if kr.Gateway != "" { kr.WhiteboxMode = true } iptablesEnv := []string{} iptablesEnv = append(iptablesEnv, env...) if !kr.WhiteboxMode { err := kr.runIptablesSetup(iptablesEnv) if err != nil { log.Println("iptables disabled ", err) kr.WhiteboxMode = true } else { log.Println("iptables interception enabled") } } else { log.Println("No iptables - starting with INTERCEPTION_MODE=NONE") } // Currently broken in iptables - use whitebox interception, but still run it if !kr.WhiteboxMode { resolvConfForRoot() env = addIfMissing(env, "ISTIO_META_DNS_CAPTURE", "true") env = addIfMissing(env, "DNS_PROXY_ADDR", "localhost:53") } // MCP config // The following 2 are required for MeshCA. env = addIfMissing(env, "GKE_CLUSTER_URL", kr.ClusterAddress) env = addIfMissing(env, "GCP_METADATA", fmt.Sprintf("%s|%s|%s|%s", kr.ProjectId, kr.ProjectNumber, kr.ClusterName, kr.ClusterLocation)) env = addIfMissing(env, "XDS_ADDR", kr.XDSAddr) //env = append(env, "CA_ROOT_CA=/etc/ssl/certs/ca-certificates.crt") //env = append(env, "XDS_ROOT_CA=/etc/ssl/certs/ca-certificates.crt") env = addIfMissing(env, "JWT_POLICY", "third-party-jwt") // Fetch ProxyConfig over XDS, merge the extra root certificates env = addIfMissing(env, "PROXY_CONFIG_XDS_AGENT", "true") env = addIfMissing(env, "TRUST_DOMAIN", kr.TrustDomain) // Gets translated to "APP_CONTAINERS" metadata, used to identify the container. env = addIfMissing(env, "ISTIO_META_APP_CONTAINERS", "cloudrun") if kr.X509KeyPair != nil { // Loaded from workload cert file - no need to use citadel or mesh CA. env = addIfMissing(env, "CA_PROVIDER", "GoogleGkeWorkloadCertificate") } // If MCP is available, and PROXY_CONFIG is not set explicitly if kr.MeshTenant != "" && kr.MeshTenant != "-" && os.Getenv("PROXY_CONFIG") == "" { env = addIfMissing(env, "CA_ADDR", "meshca.googleapis.com:443") env = addIfMissing(env, "XDS_AUTH_PROVIDER", "gcp") env = addIfMissing(env, "ISTIO_META_CLOUDRUN_ADDR", kr.MeshTenant) // Will be used to set a clusterid metadata, which will locate the remote cluster id // This is used for multi-cluster, to specify the k8s client to use for validating tokens in Istiod env = addIfMissing(env, "ISTIO_META_CLUSTER_ID", fmt.Sprintf("cn-%s-%s-%s", kr.ProjectId, kr.ClusterLocation, kr.ClusterName)) } if kr.WhiteboxMode { env = append(env, "ISTIO_META_INTERCEPTION_MODE=NONE") env = append(env, "HTTP_PROXY_PORT=15007") } // WIP: automate getting the CR addr (or have Thetis handle it) // For example by reading a configmap in cluster //--set-env-vars="ISTIO_META_CLOUDRUN_ADDR=asm-stg-asm-cr-asm-managed-rapid-c-2o26nc3aha-uc.a.run.app:443" \ // Environment detection: if the docker image or VM does not include an Envoy use the 'grpc agent' mode, // i.e. only get certificate. if _, err := os.Stat("/usr/local/bin/envoy"); os.IsNotExist(err) { env = append(env, "DISABLE_ENVOY=true") } // TODO: look in /var... if _, err := os.Stat(" ./var/lib/istio/envoy/envoy_bootstrap_tmpl.json"); os.IsNotExist(err) { if _, err := os.Stat("/var/lib/istio/envoy/envoy_bootstrap_tmpl.json"); os.IsNotExist(err) { env = append(env, "DISABLE_ENVOY=true") } else { env = append(env, "ISTIO_BOOTSTRAP=/var/lib/istio/envoy/envoy_bootstrap_tmpl.json") } } // Generate grpc bootstrap - no harm, low cost. // TODO: New version of Istio does this automatically, will be removed if os.Getenv("GRPC_XDS_BOOTSTRAP") == "" { env = append(env, "GRPC_XDS_BOOTSTRAP=./etc/istio/proxy/grpc_bootstrap.json") } cmd := kr.agentCommand() var stdout io.ReadCloser if os.Getuid() == 0 { os.MkdirAll("/etc/istio/proxy", 777) os.Chown("/etc/istio/proxy", 1337, 1337) cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{ Uid: 0, Gid: 1337, } pty, tty, err := pty.Open() if err != nil { log.Println("Error opening pty ", err) stdout, _ = cmd.StdoutPipe() os.Stdout.Chown(1337, 1337) } else { cmd.Stdout = tty err = tty.Chown(1337, 1337) if err != nil { log.Println("Error chown ", tty.Name(), err) } stdout = pty } cmd.Dir = "/" } else { cmd.Stdout = os.Stdout env = append(env, "ISTIO_META_UNPRIVILEGED_POD=true") } cmd.Env = env cmd.Stderr = os.Stderr os.MkdirAll(prefix+"/var/lib/istio/envoy/", 0700) //saveLaunchInfo(cmd) go func() { if Debug { log.Println("Starting cmd", cmd.Args) } err := cmd.Start() if err != nil { log.Println("Failed to start ", cmd, err) } kr.agentCmd = cmd if stdout != nil { go func() { io.Copy(os.Stdout, stdout) }() } err = cmd.Wait() if err != nil { if cmd.ProcessState.ExitCode() == 255 { log.Println("Wait err ", err, cmd.Env) } else { log.Println("Wait err ", err) } kr.Exit(1) } kr.Exit(0) }() return nil } // For troubleshooting, generate a file with the env and command. // This can also be used for running krun as a periodic job instead of as a launcher // Compile with -gcflags "all=-N -l" func saveLaunchInfo(cmd *exec.Cmd) { b := bytes.Buffer{} for _, e := range cmd.Env { kv := strings.SplitN(e, "=", 2) if len(kv) == 2 { b.Write([]byte("export " + kv[0] + "=" + "'" + kv[1] + "'\n")) } } b.Write([]byte{'\n'}) b.Write([]byte("dlv --listen=127.0.0.1:44997 --headless=true --api-version=2 --check-go-version=false --only-same-user=false exec ")) b.Write([]byte(cmd.Args[0])) b.Write([]byte(" -- ")) for _, e := range cmd.Args[1:] { b.Write([]byte(e)) b.Write([]byte{' '}) } b.Write([]byte{'\n'}) ioutil.WriteFile("./var/lib/istio/envoy/cmd.sh", b.Bytes(), 0700) } func addIfMissing(env []string, key, val string) []string { if os.Getenv(key) != "" { return env } return append(env, key+"="+val) } func (kr *KRun) Exit(code int) { if kr.agentCmd != nil && kr.agentCmd.Process != nil { kr.agentCmd.Process.Signal(syscall.SIGTERM) } if kr.appCmd != nil && kr.appCmd.Process != nil { kr.agentCmd.Process.Signal(syscall.SIGTERM) } for _, a := range kr.Children { a.Process.Signal(syscall.SIGTERM) } time.Sleep(5 * time.Second) if kr.agentCmd != nil && kr.agentCmd.Process != nil { kr.agentCmd.Process.Kill() } if kr.appCmd != nil && kr.appCmd.Process != nil { kr.appCmd.Process.Kill() } for _, a := range kr.Children { a.Process.Kill() } os.Exit(code) } func (kr *KRun) initLabelsFile() { labels := "" if kr.Gateway != "" { labels = fmt.Sprintf( `version="%s" security.istio.io/tlsMode="istio" istio="%s" `, kr.Rev, kr.Gateway) } else { labels = fmt.Sprintf( `version="%s" security.istio.io/tlsMode="istio" app="%s" service.istio.io/canonical-name="%s" environment="cloud-run-mesh" `, kr.Rev, kr.Name, kr.Name) } os.MkdirAll("./etc/istio/pod", 755) err := ioutil.WriteFile("./etc/istio/pod/labels", []byte(labels), 0777) if err != nil { log.Println("Error writing labels", err) } } func (kr *KRun) runIptablesSetup(env []string) error { /* Injected default: - -p - "15001" - -z - "15006" - -u - "1337" - -m - REDIRECT - -i - '*' - -x - "" - -b - '*' - -d - 15090,15021,15020 */ outRange := kr.Config("OUTBOUND_IP_RANGES_INCLUDE", "10.0.0.0/8") // Exclude ports from Envoy capture - hbone-h2, hbone-h2c excludePorts := kr.Config("OUTBOUND_PORTS_EXCLUDE", "15008,15009") if excludePorts != "15008,15009" { excludePorts = excludePorts + ",15008,15009" } cmd := exec.Command("/usr/local/bin/pilot-agent", "istio-iptables", // "-p", "15001", // outbound capture port, default value //"-z", "15006", - no inbound interception, default value "-u", "1337", // REQUIRED - code default is 128 //"-m", "REDIRECT", // default value //"-i", "*", // OUTBOUND_IP_RANGES_INCLUDE "-i", outRange, // Alternative - only mesh traffic // "-b", "", // disable all inbound redirection, default // "-d", "15090,15021,15020", // exclude specific ports from inbound capture, if -b '*' "-o", excludePorts, //"-x", "", // exclude CIDR, default ) cmd.Env = env cmd.Dir = "/" so := &bytes.Buffer{} se := &bytes.Buffer{} cmd.Stdout = so cmd.Stderr = se err := cmd.Start() if err != nil { log.Println("Error starting iptables", err, so.String(), "stderr:", se.String()) return err } else { err = cmd.Wait() if err != nil { log.Println("Error starting iptables", err, so.String(), "stderr:", se.String()) return err } } // TODO: make the stdout/stderr available in a debug endpoint return nil } // TODO: lookup istiod service and endpoints ( instead of using an ILB or external name) //