1package envoy
2
3import (
4	"errors"
5	"flag"
6	"fmt"
7	"net"
8	"os"
9	"os/exec"
10	"strings"
11
12	"github.com/mitchellh/cli"
13	"github.com/mitchellh/mapstructure"
14
15	"github.com/hashicorp/consul/agent/structs"
16	"github.com/hashicorp/consul/agent/xds"
17	"github.com/hashicorp/consul/agent/xds/proxysupport"
18	"github.com/hashicorp/consul/api"
19	proxyCmd "github.com/hashicorp/consul/command/connect/proxy"
20	"github.com/hashicorp/consul/command/flags"
21	"github.com/hashicorp/consul/ipaddr"
22	"github.com/hashicorp/consul/tlsutil"
23)
24
25func New(ui cli.Ui) *cmd {
26	c := &cmd{UI: ui}
27	c.init()
28	return c
29}
30
31const DefaultAdminAccessLogPath = "/dev/null"
32
33type cmd struct {
34	UI     cli.Ui
35	flags  *flag.FlagSet
36	http   *flags.HTTPFlags
37	help   string
38	client *api.Client
39
40	// flags
41	meshGateway           bool
42	gateway               string
43	proxyID               string
44	sidecarFor            string
45	adminAccessLogPath    string
46	adminBind             string
47	envoyBin              string
48	bootstrap             bool
49	disableCentralConfig  bool
50	grpcAddr              string
51	envoyVersion          string
52	prometheusBackendPort string
53	prometheusScrapePath  string
54
55	// mesh gateway registration information
56	register           bool
57	lanAddress         ServiceAddressValue
58	wanAddress         ServiceAddressValue
59	deregAfterCritical string
60	bindAddresses      ServiceAddressMapValue
61	exposeServers      bool
62	omitDeprecatedTags bool
63
64	gatewaySvcName string
65	gatewayKind    api.ServiceKind
66}
67
68const meshGatewayVal = "mesh"
69
70var defaultEnvoyVersion = proxysupport.EnvoyVersions[0]
71
72var supportedGateways = map[string]api.ServiceKind{
73	"mesh":        api.ServiceKindMeshGateway,
74	"terminating": api.ServiceKindTerminatingGateway,
75	"ingress":     api.ServiceKindIngressGateway,
76}
77
78func (c *cmd) init() {
79	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
80
81	c.flags.StringVar(&c.proxyID, "proxy-id", os.Getenv("CONNECT_PROXY_ID"),
82		"The proxy's ID on the local agent.")
83
84	// Deprecated in favor of `gateway`
85	c.flags.BoolVar(&c.meshGateway, "mesh-gateway", false,
86		"Configure Envoy as a Mesh Gateway.")
87
88	c.flags.StringVar(&c.gateway, "gateway", "",
89		"The type of gateway to register. One of: terminating, ingress, or mesh")
90
91	c.flags.StringVar(&c.sidecarFor, "sidecar-for", os.Getenv("CONNECT_SIDECAR_FOR"),
92		"The ID of a service instance on the local agent that this proxy should "+
93			"become a sidecar for. It requires that the proxy service is registered "+
94			"with the agent as a connect-proxy with Proxy.DestinationServiceID set "+
95			"to this value. If more than one such proxy is registered it will fail.")
96
97	c.flags.StringVar(&c.envoyBin, "envoy-binary", "",
98		"The full path to the envoy binary to run. By default will just search "+
99			"$PATH. Ignored if -bootstrap is used.")
100
101	c.flags.StringVar(&c.adminAccessLogPath, "admin-access-log-path", DefaultAdminAccessLogPath,
102		fmt.Sprintf("The path to write the access log for the administration server. If no access "+
103			"log is desired specify %q. By default it will use %q.",
104			DefaultAdminAccessLogPath, DefaultAdminAccessLogPath))
105
106	c.flags.StringVar(&c.adminBind, "admin-bind", "localhost:19000",
107		"The address:port to start envoy's admin server on. Envoy requires this "+
108			"but care must be taken to ensure it's not exposed to an untrusted network "+
109			"as it has full control over the secrets and config of the proxy.")
110
111	c.flags.BoolVar(&c.bootstrap, "bootstrap", false,
112		"Generate the bootstrap.json but don't exec envoy")
113
114	c.flags.BoolVar(&c.disableCentralConfig, "no-central-config", false,
115		"By default the proxy's bootstrap configuration can be customized "+
116			"centrally. This requires that the command run on the same agent as the "+
117			"proxy will and that the agent is reachable when the command is run. In "+
118			"cases where either assumption is violated this flag will prevent the "+
119			"command attempting to resolve config from the local agent.")
120
121	c.flags.StringVar(&c.grpcAddr, "grpc-addr", os.Getenv(api.GRPCAddrEnvName),
122		"Set the agent's gRPC address and port (in http(s)://host:port format). "+
123			"Alternatively, you can specify CONSUL_GRPC_ADDR in ENV.")
124
125	c.flags.StringVar(&c.envoyVersion, "envoy-version", defaultEnvoyVersion,
126		"Sets the envoy-version that the envoy binary has.")
127
128	c.flags.BoolVar(&c.register, "register", false,
129		"Register a new gateway service before configuring and starting Envoy")
130
131	c.flags.Var(&c.lanAddress, "address",
132		"LAN address to advertise in the gateway service registration")
133
134	c.flags.Var(&c.wanAddress, "wan-address",
135		"WAN address to advertise in the gateway service registration. For ingress gateways, "+
136			"only an IP address (without a port) is required.")
137
138	c.flags.Var(&c.bindAddresses, "bind-address", "Bind "+
139		"address to use instead of the default binding rules given as `<name>=<ip>:<port>` "+
140		"pairs. This flag may be specified multiple times to add multiple bind addresses.")
141
142	c.flags.StringVar(&c.gatewaySvcName, "service", "",
143		"Service name to use for the registration")
144
145	c.flags.BoolVar(&c.exposeServers, "expose-servers", false,
146		"Expose the servers for WAN federation via this mesh gateway")
147
148	c.flags.StringVar(&c.deregAfterCritical, "deregister-after-critical", "6h",
149		"The amount of time the gateway services health check can be failing before being deregistered")
150
151	c.flags.BoolVar(&c.omitDeprecatedTags, "omit-deprecated-tags", false,
152		"In Consul 1.9.0 the format of metric tags for Envoy clusters was updated from consul.[service|dc|...] to "+
153			"consul.destination.[service|dc|...]. The old tags were preserved for backward compatibility,"+
154			"but can be disabled with this flag.")
155
156	c.flags.StringVar(&c.prometheusBackendPort, "prometheus-backend-port", "",
157		"Sets the backend port for the 'prometheus_backend' cluster that envoy_prometheus_bind_addr will point to. "+
158			"Without this flag, envoy_prometheus_bind_addr would point to the 'self_admin' cluster where Envoy metrics are exposed. "+
159			"The metrics merging feature in consul-k8s uses this to point to the merged metrics endpoint combining Envoy and service metrics. "+
160			"Only applicable when envoy_prometheus_bind_addr is set in proxy config.")
161
162	c.flags.StringVar(&c.prometheusScrapePath, "prometheus-scrape-path", "/metrics",
163		"Sets the path where Envoy will expose metrics on envoy_prometheus_bind_addr listener. "+
164			"For example, if envoy_prometheus_bind_addr is 0.0.0.0:20200, and this flag is "+
165			"set to /scrape-metrics, prometheus metrics would be scrapeable at "+
166			"0.0.0.0:20200/scrape-metrics. "+
167			"Only applicable when envoy_prometheus_bind_addr is set in proxy config.")
168
169	c.http = &flags.HTTPFlags{}
170	flags.Merge(c.flags, c.http.ClientFlags())
171	flags.Merge(c.flags, c.http.NamespaceFlags())
172	c.help = flags.Usage(help, c.flags)
173}
174
175// canBindInternal is here mainly so we can unit test this with a constant net.Addr list
176func canBindInternal(addr string, ifAddrs []net.Addr) bool {
177	if addr == "" {
178		return false
179	}
180
181	ip := net.ParseIP(addr)
182	if ip == nil {
183		return false
184	}
185
186	ipStr := ip.String()
187
188	for _, addr := range ifAddrs {
189		switch v := addr.(type) {
190		case *net.IPNet:
191			if v.IP.String() == ipStr {
192				return true
193			}
194		default:
195			if addr.String() == ipStr {
196				return true
197			}
198		}
199	}
200
201	return false
202}
203
204func canBind(addr api.ServiceAddress) bool {
205	ifAddrs, err := net.InterfaceAddrs()
206	if err != nil {
207		return false
208	}
209
210	return canBindInternal(addr.Address, ifAddrs)
211}
212
213func (c *cmd) Run(args []string) int {
214	if err := c.flags.Parse(args); err != nil {
215		return 1
216	}
217
218	// Setup Consul client
219	var err error
220	c.client, err = c.http.APIClient()
221	if err != nil {
222		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
223		return 1
224	}
225	// TODO: refactor
226	return c.run(c.flags.Args())
227}
228
229func (c *cmd) run(args []string) int {
230	// Fixup for deprecated mesh-gateway flag
231	if c.meshGateway && c.gateway != "" {
232		c.UI.Error("The mesh-gateway flag is deprecated and cannot be used alongside the gateway flag")
233		return 1
234	}
235
236	if c.meshGateway {
237		c.gateway = meshGatewayVal
238	}
239
240	if c.exposeServers {
241		if c.gateway != meshGatewayVal {
242			c.UI.Error("'-expose-servers' can only be used for mesh gateways")
243			return 1
244		}
245		if !c.register {
246			c.UI.Error("'-expose-servers' requires '-register'")
247			return 1
248		}
249	}
250
251	// Gateway kind is set so that it is available even if not auto-registering the gateway
252	if c.gateway != "" {
253		kind, ok := supportedGateways[c.gateway]
254		if !ok {
255			c.UI.Error("Gateway must be one of: terminating, mesh, or ingress")
256			return 1
257		}
258		c.gatewayKind = kind
259
260		if c.gatewaySvcName == "" {
261			c.gatewaySvcName = string(c.gatewayKind)
262		}
263	}
264
265	if c.proxyID == "" {
266		switch {
267		case c.sidecarFor != "":
268			proxyID, err := proxyCmd.LookupProxyIDForSidecar(c.client, c.sidecarFor)
269			if err != nil {
270				c.UI.Error(err.Error())
271				return 1
272			}
273			c.proxyID = proxyID
274
275		case c.gateway != "" && !c.register:
276			gatewaySvc, err := proxyCmd.LookupGatewayProxy(c.client, c.gatewayKind)
277			if err != nil {
278				c.UI.Error(err.Error())
279				return 1
280			}
281			c.proxyID = gatewaySvc.ID
282			c.gatewaySvcName = gatewaySvc.Service
283
284		case c.gateway != "" && c.register:
285			c.proxyID = c.gatewaySvcName
286
287		}
288	}
289	if c.proxyID == "" {
290		c.UI.Error("No proxy ID specified. One of -proxy-id, -sidecar-for, or -gateway is " +
291			"required")
292		return 1
293	}
294
295	if c.register {
296		if c.gateway == "" {
297			c.UI.Error("Auto-Registration can only be used for gateways")
298			return 1
299		}
300
301		taggedAddrs := make(map[string]api.ServiceAddress)
302		lanAddr := c.lanAddress.Value()
303		if lanAddr.Address != "" {
304			taggedAddrs[structs.TaggedAddressLAN] = lanAddr
305		}
306
307		wanAddr := c.wanAddress.Value()
308		if wanAddr.Address != "" {
309			taggedAddrs[structs.TaggedAddressWAN] = wanAddr
310		}
311
312		tcpCheckAddr := lanAddr.Address
313		if tcpCheckAddr == "" {
314			// fallback to localhost as the gateway has to reside in the same network namespace
315			// as the agent
316			tcpCheckAddr = "127.0.0.1"
317		}
318
319		var proxyConf *api.AgentServiceConnectProxyConfig
320		if len(c.bindAddresses.value) > 0 {
321			// override all default binding rules and just bind to the user-supplied addresses
322			proxyConf = &api.AgentServiceConnectProxyConfig{
323				Config: map[string]interface{}{
324					"envoy_gateway_no_default_bind": true,
325					"envoy_gateway_bind_addresses":  c.bindAddresses.value,
326				},
327			}
328		} else if canBind(lanAddr) && canBind(wanAddr) {
329			// when both addresses are bindable then we bind to the tagged addresses
330			// for creating the envoy listeners
331			proxyConf = &api.AgentServiceConnectProxyConfig{
332				Config: map[string]interface{}{
333					"envoy_gateway_no_default_bind":       true,
334					"envoy_gateway_bind_tagged_addresses": true,
335				},
336			}
337		} else if !canBind(lanAddr) && lanAddr.Address != "" {
338			c.UI.Error(fmt.Sprintf("The LAN address %q will not be bindable. Either set a bindable address or override the bind addresses with -bind-address", lanAddr.Address))
339			return 1
340		}
341
342		var meta map[string]string
343		if c.exposeServers {
344			meta = map[string]string{structs.MetaWANFederationKey: "1"}
345		}
346
347		svc := api.AgentServiceRegistration{
348			Kind:            c.gatewayKind,
349			Name:            c.gatewaySvcName,
350			ID:              c.proxyID,
351			Address:         lanAddr.Address,
352			Port:            lanAddr.Port,
353			Meta:            meta,
354			TaggedAddresses: taggedAddrs,
355			Proxy:           proxyConf,
356			Check: &api.AgentServiceCheck{
357				Name:                           fmt.Sprintf("%s listening", c.gatewayKind),
358				TCP:                            ipaddr.FormatAddressPort(tcpCheckAddr, lanAddr.Port),
359				Interval:                       "10s",
360				DeregisterCriticalServiceAfter: c.deregAfterCritical,
361			},
362		}
363
364		if err := c.client.Agent().ServiceRegister(&svc); err != nil {
365			c.UI.Error(fmt.Sprintf("Error registering service %q: %s", svc.Name, err))
366			return 1
367		}
368
369		if !c.bootstrap {
370			// We need stdout to be reserved exclusively for the JSON blob, so
371			// we omit logging this to Info which also writes to stdout.
372			c.UI.Info(fmt.Sprintf("Registered service: %s", svc.Name))
373		}
374	}
375
376	// Generate config
377	bootstrapJson, err := c.generateConfig()
378	if err != nil {
379		c.UI.Error(err.Error())
380		return 1
381	}
382
383	if c.bootstrap {
384		// Just output it and we are done
385		c.UI.Output(string(bootstrapJson))
386		return 0
387	}
388
389	// Find Envoy binary
390	binary, err := c.findBinary()
391	if err != nil {
392		c.UI.Error("Couldn't find envoy binary: " + err.Error())
393		return 1
394	}
395
396	err = execEnvoy(binary, nil, args, bootstrapJson)
397	if err == errUnsupportedOS {
398		c.UI.Error("Directly running Envoy is only supported on linux and macOS " +
399			"since envoy itself doesn't build on other platforms currently.")
400		c.UI.Error("Use the -bootstrap option to generate the JSON to use when running envoy " +
401			"on a supported OS or via a container or VM.")
402		return 1
403	} else if err != nil {
404		c.UI.Error(err.Error())
405		return 1
406	}
407
408	return 0
409}
410
411var errUnsupportedOS = errors.New("envoy: not implemented on this operating system")
412
413func (c *cmd) findBinary() (string, error) {
414	if c.envoyBin != "" {
415		return c.envoyBin, nil
416	}
417	return exec.LookPath("envoy")
418}
419
420func (c *cmd) templateArgs() (*BootstrapTplArgs, error) {
421	httpCfg := api.DefaultConfig()
422	c.http.MergeOntoConfig(httpCfg)
423
424	// api.NewClient normalizes some values (Token, Scheme) on the Config.
425	if _, err := api.NewClient(httpCfg); err != nil {
426		return nil, err
427	}
428
429	grpcAddr, err := c.grpcAddress(httpCfg)
430	if err != nil {
431		return nil, err
432	}
433
434	adminAddr, adminPort, err := net.SplitHostPort(c.adminBind)
435	if err != nil {
436		return nil, fmt.Errorf("Invalid Consul HTTP address: %s", err)
437	}
438
439	// Envoy requires IP addresses to bind too when using static so resolve DNS or
440	// localhost here.
441	adminBindIP, err := net.ResolveIPAddr("ip", adminAddr)
442	if err != nil {
443		return nil, fmt.Errorf("Failed to resolve admin bind address: %s", err)
444	}
445
446	// Ideally the cluster should be the service name. We may or may not have that
447	// yet depending on the arguments used so make a best effort here. In the
448	// common case, even if the command was invoked with proxy-id and we don't
449	// know service name yet, we will after we resolve the proxy's config in a bit
450	// and will update this then.
451	cluster := c.proxyID
452	proxySourceService := ""
453	if c.sidecarFor != "" {
454		cluster = c.sidecarFor
455		proxySourceService = c.sidecarFor
456	} else if c.gateway != "" && c.gatewaySvcName != "" {
457		cluster = c.gatewaySvcName
458		proxySourceService = c.gatewaySvcName
459	}
460
461	adminAccessLogPath := c.adminAccessLogPath
462	if adminAccessLogPath == "" {
463		adminAccessLogPath = DefaultAdminAccessLogPath
464	}
465
466	var caPEM string
467	pems, err := tlsutil.LoadCAs(httpCfg.TLSConfig.CAFile, httpCfg.TLSConfig.CAPath)
468	if err != nil {
469		return nil, err
470	}
471	caPEM = strings.Replace(strings.Join(pems, ""), "\n", "\\n", -1)
472
473	return &BootstrapTplArgs{
474		GRPC:                  grpcAddr,
475		ProxyCluster:          cluster,
476		ProxyID:               c.proxyID,
477		ProxySourceService:    proxySourceService,
478		AgentCAPEM:            caPEM,
479		AdminAccessLogPath:    adminAccessLogPath,
480		AdminBindAddress:      adminBindIP.String(),
481		AdminBindPort:         adminPort,
482		Token:                 httpCfg.Token,
483		LocalAgentClusterName: xds.LocalAgentClusterName,
484		Namespace:             httpCfg.Namespace,
485		EnvoyVersion:          c.envoyVersion,
486		Datacenter:            httpCfg.Datacenter,
487		PrometheusBackendPort: c.prometheusBackendPort,
488		PrometheusScrapePath:  c.prometheusScrapePath,
489	}, nil
490}
491
492func (c *cmd) generateConfig() ([]byte, error) {
493	args, err := c.templateArgs()
494	if err != nil {
495		return nil, err
496	}
497
498	var bsCfg BootstrapConfig
499
500	// Setup ready listener for ingress gateway to pass healthcheck
501	if c.gatewayKind == api.ServiceKindIngressGateway {
502		lanAddr := c.lanAddress.String()
503		// Deal with possibility of address not being specified and defaulting to
504		// ":443"
505		if strings.HasPrefix(lanAddr, ":") {
506			lanAddr = "127.0.0.1" + lanAddr
507		}
508		bsCfg.ReadyBindAddr = lanAddr
509	}
510
511	// Fetch any customization from the registration
512	svc, _, err := c.client.Agent().Service(c.proxyID, nil)
513	if err != nil {
514		return nil, fmt.Errorf("failed fetch proxy config from local agent: %s", err)
515	}
516	if svc.Proxy == nil {
517		return nil, errors.New("service is not a Connect proxy or gateway")
518	}
519
520	if svc.Proxy.DestinationServiceName != "" {
521		// Override cluster now we know the actual service name
522		args.ProxyCluster = svc.Proxy.DestinationServiceName
523		args.ProxySourceService = svc.Proxy.DestinationServiceName
524	} else {
525		// Set the source service name from the proxy's own registration
526		args.ProxySourceService = svc.Service
527	}
528	if svc.Namespace != "" {
529		// In most cases where namespaces are enabled this will already be set
530		// correctly because the http client that fetched this will need to have
531		// had the namespace set on it which is also how we initially populate
532		// this. However in the case of "default" namespace being accessed because
533		// there was no namespace argument, args.Namespace will be empty even
534		// though Namespaces are actually being used and the namespace of the request was
535		// inferred from the ACL token or defaulted to the "default" namespace.
536		// Overriding it here ensures that we always set the Namespace arg if the
537		// cluster is using namespaces regardless.
538		args.Namespace = svc.Namespace
539	}
540
541	if svc.Datacenter != "" {
542		// The agent will definitely have the definitive answer here.
543		args.Datacenter = svc.Datacenter
544	}
545
546	if !c.disableCentralConfig {
547		// Parse the bootstrap config
548		if err := mapstructure.WeakDecode(svc.Proxy.Config, &bsCfg); err != nil {
549			return nil, fmt.Errorf("failed parsing Proxy.Config: %s", err)
550		}
551	}
552
553	return bsCfg.GenerateJSON(args, c.omitDeprecatedTags)
554}
555
556// TODO: make method a function
557func (c *cmd) grpcAddress(httpCfg *api.Config) (GRPC, error) {
558	g := GRPC{}
559
560	addr := c.grpcAddr
561	// See if we need to lookup grpcAddr
562	if addr == "" {
563		port, err := c.lookupGRPCPort()
564		if err != nil {
565			c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
566		}
567		if port <= 0 {
568			// This is the dev mode default and recommended production setting if
569			// enabled.
570			port = 8502
571		}
572		addr = fmt.Sprintf("localhost:%v", port)
573	}
574
575	// TODO: parse addr as a url instead of strings.HasPrefix/TrimPrefix
576
577	// Decide on TLS if the scheme is provided and indicates it, if the HTTP env
578	// suggests TLS is supported explicitly (CONSUL_HTTP_SSL) or implicitly
579	// (CONSUL_HTTP_ADDR) is https://
580	switch {
581	case strings.HasPrefix(strings.ToLower(addr), "https://"):
582		g.AgentTLS = true
583	case httpCfg.Scheme == "https":
584		g.AgentTLS = true
585	}
586
587	// We want to allow grpcAddr set as host:port with no scheme but if the host
588	// is an IP this will fail to parse as a URL with "parse 127.0.0.1:8500: first
589	// path segment in URL cannot contain colon". On the other hand we also
590	// support both http(s)://host:port and unix:///path/to/file.
591	if grpcAddr := strings.TrimPrefix(addr, "unix://"); grpcAddr != addr {
592		// Path to unix socket
593		g.AgentSocket = grpcAddr
594	} else {
595		// Parse as host:port with option http prefix
596		grpcAddr = strings.TrimPrefix(addr, "http://")
597		grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
598
599		var err error
600		var host string
601		host, g.AgentPort, err = net.SplitHostPort(grpcAddr)
602		if err != nil {
603			return g, fmt.Errorf("Invalid Consul HTTP address: %s", err)
604		}
605
606		// We use STATIC for agent which means we need to resolve DNS names like
607		// `localhost` ourselves. We could use STRICT_DNS or LOGICAL_DNS with envoy
608		// but Envoy resolves `localhost` differently to go on macOS at least which
609		// causes paper cuts like default dev agent (which binds specifically to
610		// 127.0.0.1) isn't reachable since Envoy resolves localhost to `[::]` and
611		// can't connect.
612		agentIP, err := net.ResolveIPAddr("ip", host)
613		if err != nil {
614			return g, fmt.Errorf("Failed to resolve agent address: %s", err)
615		}
616		g.AgentAddress = agentIP.String()
617	}
618	return g, nil
619}
620
621func (c *cmd) lookupGRPCPort() (int, error) {
622	self, err := c.client.Agent().Self()
623	if err != nil {
624		return 0, err
625	}
626	cfg, ok := self["DebugConfig"]
627	if !ok {
628		return 0, fmt.Errorf("unexpected agent response: no debug config")
629	}
630	port, ok := cfg["GRPCPort"]
631	if !ok {
632		return 0, fmt.Errorf("agent does not have grpc port enabled")
633	}
634	portN, ok := port.(float64)
635	if !ok {
636		return 0, fmt.Errorf("invalid grpc port in agent response")
637	}
638
639	return int(portN), nil
640}
641
642func (c *cmd) Synopsis() string {
643	return synopsis
644}
645
646func (c *cmd) Help() string {
647	return c.help
648}
649
650const synopsis = "Runs or Configures Envoy as a Connect proxy"
651const help = `
652Usage: consul connect envoy [options]
653
654  Generates the bootstrap configuration needed to start an Envoy proxy instance
655  for use as a Connect sidecar for a particular service instance. By default it
656  will generate the config and then exec Envoy directly until it exits normally.
657
658  It will search $PATH for the envoy binary but this can be overridden with
659  -envoy-binary.
660
661  It can instead only generate the bootstrap.json based on the current ENV and
662  arguments using -bootstrap.
663
664  The proxy requires service:write permissions for the service it represents.
665  The token may be passed via the CLI or the CONSUL_HTTP_TOKEN environment
666  variable.
667
668  The example below shows how to start a local proxy as a sidecar to a "web"
669  service instance. It assumes that the proxy was already registered with it's
670  Config for example via a sidecar_service block.
671
672    $ consul connect envoy -sidecar-for web
673
674`
675