Consul Connect isn’t just about encrypting traffic; it’s fundamentally about authorizing services to talk to each other, even when you have no idea who they are or where they’re running.

Imagine you have two services, frontend and backend, that need to communicate. Normally, you’d open firewall ports, maybe set up TLS between them, but you’re still implicitly trusting any process that can reach backend on its port. Consul Connect flips this: instead of opening doors, you’re issuing specific, revocable IDs to services and defining exactly which other IDs they’re allowed to talk to.

Let’s see it in action. We’ll start with two simple Go services, echo-server and echo-client.

echo-server (Go):

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // Default port
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from echo-server on port %s!\n", port)
	})

	log.Printf("Starting echo-server on port %s...", port)
	err := http.ListenAndServe(":"+port, nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

echo-client (Go):

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func main() {
	backendAddr := os.Getenv("BACKEND_ADDR")
	if backendAddr == "" {
		log.Fatal("BACKEND_ADDR environment variable not set")
	}

	resp, err := http.Get("http://" + backendAddr)
	if err != nil {
		log.Fatalf("Error making request to backend: %v", err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Error reading response body: %v", err)
	}

	fmt.Printf("Client received: %s\n", body)
}

First, we need to register these services with Consul. We’ll use the Consul CLI for this.

1. Register echo-server:

consul services register -name echo-server -port 8080 -id echo-server-1

This tells Consul about our server, its name (echo-server), the port it listens on (8080), and a unique ID (echo-server-1).

2. Register echo-client:

consul services register -name echo-client -port 0 -id echo-client-1

The client doesn’t need to expose a port for other services to connect to it, so we use 0.

Now, let’s enable Consul Connect for them. This involves defining an intention, which is the core authorization mechanism.

3. Define an Intention: We want echo-client to be allowed to talk to echo-server.

consul intentions create -from echo-client-1 -to echo-server-1 -verb http

This command grants permission. FROM specifies the source service ID, TO specifies the destination service ID, and VERB specifies the HTTP method.

To actually make them talk through Connect, we need to use the consul connect command to proxy the connection.

4. Run echo-server via Consul Connect:

consul connect \
  -sidecar \
  -proxy ./proxy.json \
  -register -name echo-server -id echo-server-1 \
  -auto-register \
  -service-id echo-server-1 \
  -upstreams "backend:8080"

# Create a simple proxy.json for the server
echo '{
  "services": [
    {
      "name": "echo-server",
      "port": 8080
    }
  ]
}' > proxy.json

This command tells Consul to:

  • Run as a sidecar proxy.
  • Use the configuration in proxy.json.
  • Register the service with Consul (-register, -name, -id).
  • Automatically register the proxy itself (-auto-register).
  • Link this proxy to the service ID echo-server-1.
  • Define an upstream connection named backend that will connect to local port 8080.

5. Run echo-client via Consul Connect:

consul connect \
  -sidecar \
  -proxy ./proxy.json \
  -register -name echo-client -id echo-client-1 \
  -auto-register \
  -service-id echo-client-1 \
  -upstreams "backend:8080"

# Create a simple proxy.json for the client
echo '{
  "services": [
    {
      "name": "echo-client",
      "port": 0
    }
  ]
}' > proxy.json

This is similar, but the client’s proxy.json specifies port: 0 because it doesn’t need to listen externally. The -upstreams here tells the client’s sidecar proxy that it needs to be able to reach a service named backend on port 8080.

Now, we need to tell our actual application code where to find the service it needs. This is done via environment variables that the consul connect proxy sets. The consul connect command injects environment variables for each upstream service defined in the proxy.json. The pattern is UPSTREAM_NAME_ADDR and UPSTREAM_NAME_PORT.

So, to run our client application, we need to set BACKEND_ADDR to the address provided by the client’s sidecar proxy for the backend upstream.

6. Run the echo-client application: First, find the address the client sidecar proxy has exposed for the backend upstream. You can inspect the environment variables set by the consul connect command, or if you run the client app directly, you’ll see it. For simplicity, let’s assume the client sidecar proxy is listening on localhost:21000 for the backend upstream.

# Assuming the client sidecar proxy exposes backend on localhost:21000
export BACKEND_ADDR="localhost:21000"
go run echo-client/main.go

If everything is configured correctly, you’ll see:

Client received: Hello from echo-server on port 8080!

The magic here is that echo-client isn’t talking directly to echo-server on port 8080. It’s talking to its own local sidecar proxy (on localhost:21000), which then uses Consul’s service discovery and the intention rules to find an available echo-server instance, establish a secure, mutually TLS-encrypted connection to its sidecar proxy, and forward the request. The server sidecar then forwards it to the actual echo-server application.

The proxy.json is crucial. It tells the sidecar proxy what service it represents and what upstream services it needs to connect to. The -upstreams flag in the consul connect command maps a logical upstream name (like backend in the client’s case) to a network address and port that the application should use. The consul connect proxy then intercepts this connection, finds the destination service via Consul’s catalog, establishes a secure connection, and forwards the request.

This setup provides mutual TLS encryption between the sidecar proxies, service identity verification, and authorization based on Consul intentions, all without modifying the application code itself. It’s about abstracting network security and service-to-service communication away from the application developers.

The next step in understanding Consul Connect is exploring how to manage these intentions at scale, perhaps using the Consul API or Terraform, and how to handle scenarios with multiple instances of the same service.

Want structured learning?

Take the full Consul course →