skip to content

Services inside a Consul service mesh must call a third-party API and a legacy database that will never run a sidecar. What does a Consul terminating gateway do for that traffic, and what control do you keep once the traffic leaves it?

level: seniorimportance: should knowfreq 26%

answer

  1. the mesh's sanctioned way out
  2. mTLS in, ordinary connection out
  3. external services registered in the catalog
  4. intentions still name the outside destination
  5. one hop, one chokepoint, one set of egress IPs

basics

~20 s

A Consul terminating gateway is a mesh member that accepts mTLS from sidecars and forwards to destinations that are not in the mesh, optionally originating TLS to them. Intentions still govern who may reach each destination, but past the gateway mesh identity is gone.

solid answer

~60 s

A terminating gateway is the mesh's sanctioned egress door. It registers as a mesh participant, so a sidecar reaches it over ordinary mTLS with a normal service identity, and the gateway then opens a connection to a destination that has no sidecar — a SaaS API, a legacy database, anything outside. The `terminating-gateway` config entry lists which external services it will front, and per service you can supply `CAFile`, `CertFile`, `KeyFile` and `SNI` so the gateway originates TLS on the outbound leg rather than sending plaintext. Authorization is preserved on the inbound side: intentions naming the external service as destination decide which mesh services may use it, so egress becomes a policy you write rather than a hole you leave open. What you lose is everything past the gateway — the third party sees the gateway's address, not the caller's identity, so any per-caller distinction there must be carried some other way. The gateway is also a chokepoint: it needs capacity, redundancy and its own token, and it is where you would enforce an outbound IP allowlist.

code

hcl · 13 lines
hcl
Kind = "terminating-gateway"
Name = "my-terminating-gateway"

Services = [
  {
    Name   = "stripe-api"
    CAFile = "/etc/ssl/certs/ca-bundle.crt"
    SNI    = "api.stripe.com"
  },
  {
    Name = "legacy-oracle"
  }
]

go deeper

for a junior

Know that a terminating gateway is how mesh services reach things outside the mesh, and that the mesh's encrypted, identified connection stops at the gateway.

for a middle

Describe the config entry listing external services, the optional CAFile/SNI that make the gateway originate TLS outbound, and that external destinations must be registered in the catalog first.

for a senior

Reason about both legs and the blast radius: what the third party actually sees, why the gateway is a chokepoint needing redundancy and credentials, and how you prove nothing is leaving by another route.

for a principal

Own the egress model for the platform — gateway versus direct destinations, where outbound credentials live, how external dependencies get inventoried and reviewed, and what the third party's IP allowlist commits you to.

## The problem it solves A mesh gives you identity, encryption and authorization *between mesh members*. The moment a service must call something that will never run a sidecar — a payment provider, a mainframe, a database appliance — the model runs out. You have three unattractive options: let sidecars dial straight out (no policy, no visibility), poke holes in the network layer (policy exists but is expressed in addresses, not services), or route the traffic through something that belongs to the mesh on one side and to the outside world on the other. The **terminating gateway** is that third option. ## How it fits together The gateway is itself a registered Consul service running a proxy, typically started with `consul connect envoy -gateway=terminating -register -service my-terminating-gateway`. On the inbound side it is an ordinary mesh peer: a calling service's sidecar opens a mutual-TLS connection to it exactly as it would to any other mesh service. What it may forward to is declared in a config entry: ```hcl Kind = "terminating-gateway" Name = "my-terminating-gateway" Services = [ { Name = "stripe-api" CAFile = "/etc/ssl/certs/ca-bundle.crt" SNI = "api.stripe.com" }, { Name = "legacy-oracle" } ] ``` The names in that list refer to services registered in the Consul catalog as **external** services — catalog entries with an address and port but no local agent and no sidecar. Registering them is what lets the rest of the mesh talk about `stripe-api` as a first-class destination rather than as a URL buried in application configuration. ## The two TLS legs This is the part worth being precise about, because "terminating" is easy to misread. The gateway **terminates** the mesh's mTLS from the caller — that leg ends here. It then makes a second, entirely separate connection outward. If the entry supplies `CAFile` (and optionally `CertFile`/`KeyFile` for client certificates) plus an `SNI`, the gateway originates TLS on that outbound leg and validates the destination against the given CA. If you supply nothing, the outbound leg is whatever the destination expects — often plaintext, which is fine for a database on a trusted network and not fine for anything crossing the internet. Being able to say "mesh identity ends at the gateway; the outbound leg is a fresh connection with its own trust settings" is the answer an interviewer is listening for. ## What you keep Intentions still apply, with the external service as destination: ```hcl Kind = "service-intentions" Name = "stripe-api" Sources = [ { Name = "billing", Action = "allow" }, { Name = "*", Action = "deny" } ] ``` So "only the billing service may reach the payment provider" becomes a written, reviewable rule keyed on service identity rather than on which pods happen to sit behind which NAT address. You also gain a single place that sees all outbound traffic: gateway metrics and access logs give you an inventory of what your platform actually depends on externally, which is usually more surprising than teams expect. And because all egress leaves from a known set of hosts, the third party's IP allowlist has a small, stable set of addresses to hold. ## What you lose Everything downstream of the gateway is anonymous. The provider sees the gateway, so if you need per-service attribution at the destination you must carry it in the request itself — an API key, a token, a header — and that is application-level work the mesh will not do for you. The gateway is also a genuine chokepoint. It needs to be run redundantly and sized for aggregate egress; a single instance is a single point of failure for every external dependency at once. It holds credentials — client certificates, and its own ACL token — making it a higher-value target than an ordinary sidecar. And it introduces a hop, which means one more place where a timeout can be misconfigured relative to the caller's. ## The alternative worth naming Not all egress has to go through a gateway. Consul can also model external destinations directly on `service-defaults` with a `Destination` block giving addresses and a port, letting a transparent-proxy sidecar dial out to a known external endpoint without a gateway hop. That is simpler for a handful of well-known destinations and avoids the chokepoint, but it gives up the central inventory, the shared client credentials and the stable egress addresses. Relatedly, the `mesh` config entry can set `TransparentProxy { MeshDestinationsOnly = true }`, which makes traffic to anything not modelled fail loudly instead of leaking out silently — the setting that turns "we have a gateway" into "the gateway is actually the only way out".

  • Does the third-party API see which mesh service originated the call?
    No. The mesh's mutual TLS ends at the gateway, and the outbound leg is a fresh connection from the gateway's own address using whatever credentials the gateway holds. Any per-caller distinction at the destination has to be carried in the request — a scoped API key or token issued per consuming service — which is application work the mesh does not do for you.
  • How do you stop a sidecar from simply dialling an external address and skipping the gateway entirely?
    Model it as policy rather than hoping. The `mesh` config entry's `TransparentProxy { MeshDestinationsOnly = true }` makes redirected traffic to anything not modelled as a mesh destination fail rather than leave silently. Below that, network-level egress rules that permit outbound only from the gateway hosts turn the gateway into the sole physical path out.
  • When would you skip the terminating gateway for an external dependency?
    When there are only a handful of well-known destinations and the hop is not worth it. Consul can model an external endpoint directly on `service-defaults` with a `Destination` block, letting a transparent-proxy sidecar reach it without a gateway. You trade away the central egress inventory, shared client credentials and a stable set of source addresses in exchange for one less chokepoint.

saying these in an interview costs you the question

  • Thinks the mesh's mTLS continues past the gateway to the destination
  • Assumes the external service sees the calling service's identity
  • Believes intentions stop applying to non-mesh destinations
  • Runs a single gateway instance for all external dependencies
  • Confuses it with an ingress gateway for inbound traffic

context