Skip to main content
Version: 5.x

MTE API Relay on Azure

Introduction​

MTE API Relay protects HTTP traffic between server applications with two relays. Your service sends ordinary HTTP to an outbound relay, which encrypts each request and sends it to an inbound relay in front of the destination API. The inbound relay decrypts it, calls the API, and encrypts the response for the trip back.

Typical Use Case

Both relays in the diagram are MTE API Relay instances. The outbound relay sits next to the calling API and the inbound relay next to the target API.

info

Both relays of a chain run the MTE API Relay image, which pairs only with another MTE API Relay. It cannot pair with an MTE Relay Server or with the client libraries. v5 relays pair only with v5 relays; see Upgrading from v4.

On Azure, MTE API Relay is a Kubernetes app on Azure Marketplace. You install each relay with a six tab wizard in the Azure portal. The wizard creates an Azure Kubernetes Service (AKS) cluster or uses one you have, installs the relay as a cluster extension in its own namespace, and stores the relay's first route, so the relay is serving when the deployment finishes. The Relay role field on the wizard's Relay tab picks what the install runs:

  • Inbound sits in front of your API: the relay pods, scaled by a horizontal pod autoscaler, the control plane that configures them, and optionally one Valkey pod for shared pair state.
  • Outbound sits beside the services that call an API through its inbound relay: relay pods only. They take their one route from the wizard, keep their pairs in memory, and never face the internet.

The offer bills through your Azure subscription at the plan's hourly price for every running relay pod and for the control plane pod of an inbound install, so the subscription is the license. The price is on the offer's Plans + Pricing tab. A pod bills from the moment it runs, ready or not: an inbound install with one relay pod bills two pods, an outbound install with one relay pod bills one, and the default range of two to six relay pods bills three to seven inbound or two to six outbound.

For MTE API Relay v4 on Azure, see the MTE API Relay v4 Azure guide.


Before you start​

  • An Azure subscription that can buy Marketplace software. Pay as you go and Enterprise Agreement subscriptions can. Free credits and some sponsorship subscriptions cannot.
  • For an inbound relay, your API's address, such as https://api.example.com, reachable from the cluster, and an admin password for the control plane, 16 characters or more, kept in your password manager.
  • For an outbound relay, the inbound relay's address, such as https://partner-relay.example.com. Install the inbound relay first.
  • An AKS cluster per side, or none. The wizard can create one. An existing cluster needs Kubernetes 1.30 or later, a managed identity, and Linux amd64 nodes.
  • A shell for the checks after the install. Azure Cloud Shell (Bash) in the portal already has az and kubectl. On your own machine, install the Azure CLI and kubectl. To open the control plane in a browser, you need kubectl on a machine that also has the browser.

If the cluster restricts egress, allow outbound HTTPS to <region>.dp.kubernetesconfiguration.azure.com, mcr.microsoft.com, *.data.mcr.microsoft.com, arcmktplaceprod.azurecr.io and its regional *.data.azurecr.io endpoints, *.ingestion.msftcloudes.com, and *.microsoftmetrics.com, plus the inbound relay or your API and, with Azure Managed Redis, port 10000 to the cache.


Install from Azure Marketplace​

In the Azure portal, open Marketplace, search for MTE API Relay, open the Eclypses offer, pick the plan, and choose Create. From an existing cluster you can also open Extensions + applications, choose Add, and pick the offer.

Run the wizard once per relay: first for the inbound relay, then for the outbound relay on the calling side. The tabs are the same for both; the Relay role on the Relay tab changes what the Relay and Control plane tabs ask for.

1. Basics​

Basics tab: subscription, resource group, region, and whether to create a new AKS cluster

FieldWhat to enter
SubscriptionThe subscription that pays for the plan.
Resource groupFor an existing cluster, the cluster's own resource group: the next tab lists only the clusters in it. For a new cluster, any resource group, or Create new.
RegionWhere a new cluster goes. An existing cluster keeps its own region, so this field does nothing for it.
Create a new AKS cluster?No installs into a cluster you already have. Yes creates one first, with the settings on the next tab.

2. Cluster​

With an existing cluster, pick it from AKS cluster. If the tab says the resource group has no AKS cluster, go back to Basics and choose the resource group the cluster lives in.

With a new cluster, the tab shows its settings:

Cluster tab for a new cluster: name, Kubernetes version, pricing tier, availability zones, node operating system, and the system and relay node sizes

FieldWhat to choose
Cluster nameDefaults to mte-relay. 2 to 54 letters, numbers, and hyphens. It is also the cluster's DNS prefix.
Kubernetes versionRequired. The list holds the generally available versions in the region, newest first. Pick the first one.
Pricing tierStandard, the default, carries Microsoft's uptime SLA for the Kubernetes API server. Free has none; use it only to evaluate.
Spread nodes across availability zones 1, 2 and 3On by default. Keeps the relay running through a zone outage. Turn it off only in a region without zones; it cannot change later.
Node operating systemAzure Linux by default, or Ubuntu.
System node sizeDefaults to Standard_D2s_v5. Two system nodes run the cluster's own add ons. A taint keeps the relay off them.
Relay node sizeDefaults to Standard_D2als_v6. The relay pool runs the relay pods and, inbound, the control plane and Valkey. A 2 vCPU, 4 GiB node fits one medium relay pod. The large pod size needs 4 vCPU nodes.

You do not pick a node count. The relay pool autoscales between two nodes, or one for a single relay pod, and one node more than the relay pod maximum on the next tab: two to seven nodes with the defaults. The new cluster also gets Azure CNI Overlay with Cilium network policy, workload identity, and the stable automatic upgrade channel with NodeImage node updates. Creating a cluster adds about five minutes to the install.

3. Relay​

FieldWhat to choose
Relay roleInbound, the default, receives from an outbound relay and calls your API. Outbound encrypts your services' calls to an inbound relay.
Extension nameDefaults to mte-relay-mar. The extension's name on the cluster. Each install in a cluster needs its own.
NamespaceDefaults to mte-relay-mar. One install per namespace. An inbound and an outbound install in one cluster need different namespaces; this page uses mte-relay-mar and mte-relay-mar-out.

Inbound​

Relay tab with the Inbound role: extension name, namespace, and the API address

API address is the only required field. It is where the inbound relay forwards decrypted requests, such as https://api.example.com. Use http:// only for an API on a private network. The address becomes a single route that matches every host name, so whatever name the outbound relay calls, the inbound relay forwards to this API. Add host specific routes later in the control plane, under Proxies.

The Network section places the relay:

Relay tab, Network section: how the relay is exposed, the load balancer port, allowed client address ranges, and a DNS name label

FieldWhat to choose
Expose the relay throughPublic load balancer, the default, gives the relay a public IP address. Internal load balancer gives it a private address in the cluster's virtual network. Cluster only gives it no address outside the cluster, for Application Gateway for Containers or your own ingress.
Load balancer portDefaults to 80. The port outbound relays connect to. The relay listens on 8080 inside the pod.
Allowed client address rangesEmpty by default, which admits every address. IPv4 CIDRs the load balancer admits, separated by commas, such as the outbound side's egress addresses.
DNS name labelPublic load balancer only. Names the address <label>.<region>.cloudapp.azure.com. The label must be unique in the region.

Both load balancers carry plain HTTP, and the tab warns about it. MTE encrypts every request and response body, but URLs and headers travel in the clear, and an outbound relay calls only https:// addresses unless you allow http:// on its side. For production, choose Cluster only and put Application Gateway for Containers with TLS in front, as in Production setup. You can switch later without reinstalling.

The Scaling section sizes it:

Relay tab, Scaling section: shared pair state, autoscaling, the relay pod range, target CPU, and pod size

FieldWhat to choose
Shared pair stateNone runs exactly one relay pod. Valkey in the cluster, the default, runs one Valkey pod, which is not billed, so two or more relay pods can serve any outbound relay. Existing Redis uses a cache you run, such as Azure Managed Redis.
Scale relay pods on CPUOn by default. A horizontal pod autoscaler adds a relay pod when average CPU passes the target, and removes one a minute after five quiet minutes. Off runs the minimum.
Relay pods, minimumDefaults to 2. The autoscaler's floor, 1 to 20. Two or more keep the relay up during updates and node restarts.
Relay pods, maximumDefaults to 6. The autoscaler's ceiling, 1 to 50, and so the most relay pods billed in an hour.
Target average CPUDefaults to 80. Percent of each pod's CPU request, 30 to 90.
Relay pod sizeMedium by default. Small requests 0.5 vCPU and 512 MiB, Medium 1 vCPU and 1 GiB, Large 2 vCPU and 2 GiB; each may use twice its request. A new cluster with 2 vCPU relay nodes offers Small and Medium only.

With None, the pod fields disappear: the relay runs one pod. Valkey keeps pair state in memory, so if its pod restarts, the outbound relays pair again. For a zone redundant store, choose Existing Redis:

Shared pair state set to Existing Redis: host name, port 10000, Connect with TLS, and the access key

The cache needs the Enterprise or Non clustered clustering policy, never OSS, access key authentication, and a network path from the cluster. Azure Managed Redis below creates one. Port 10000 and Connect with TLS are Azure Managed Redis's own settings.

Outbound​

Relay tab with the Outbound role: the inbound relay address, the http option, and Cluster only exposure

FieldWhat to choose
Inbound relay addressRequired. The inbound relay's address, such as https://partner-relay.example.com. Your services call the outbound relay with plain HTTP, and it forwards every request there, encrypted.
Allow an http:// inbound addressOff by default. Required for an http:// address. Use it only for an inbound relay on a private network, such as an internal load balancer in a peered virtual network, or the same cluster. MTE still encrypts every body.
Expose the relay throughCluster only, the default, serves services in this cluster on port 8080. Internal load balancer also serves the cluster's virtual network, on the load balancer port. An outbound relay is never public.

The outbound role has no Redis field. Each outbound pod keeps its own pairs in memory and pairs with the inbound relay on its own, so the pods scale freely with the same autoscaling, pod range, and pod size fields as above. Keep the outbound relay private: anything that reaches it can call the inbound relay's API.

4. Control plane​

Inbound installs the control plane:

Control plane tab for an inbound relay: admin password, how administrators reach the panel, the settings disk, and Prometheus metrics

FieldWhat to choose
Admin passwordRequired, 16 to 128 characters. You sign in to the panel as admin with it.
Reach the control plane throughkubectl port-forward only, the default, opens nothing: administrators reach the panel through kubectl. Internal load balancer gives it a private address on port 8081 in the cluster's virtual network, for administrators on a VPN or a peered network. It serves plain HTTP, so use it only on a network you trust. The panel is never public.
Allowed administrator address rangesEmpty by default, which admits the whole virtual network. Internal load balancer only. Private IPv4 CIDRs, separated by commas, such as your VPN's client range.
Settings disk1 GiB by default, or 4 GiB. The Azure Disk that holds the panel's settings and the last 50 versions of the configuration. A few megabytes are used. It can grow later, not shrink.
Expose Prometheus metrics on port 9090Off by default. Relay pods and the panel serve /metrics on port 9090 inside the cluster, behind a bearer token in the Secret mte-api-relay-metrics.

Outbound has no control plane, so only the metrics option is left:

Control plane tab for an outbound relay: only the Prometheus metrics option

5. Updates and tags​

Updates and tags tab: automatic minor and patch upgrades, and tags for a new cluster

FieldWhat to choose
Install new minor and patch versions automaticallyOn by default. Azure upgrades the extension to new 5.x releases, security fixes included. Major versions always wait for you. Keep both relays of a chain on the same version.
TagsShown only for a new cluster, and applied to it. Extensions take no tags.

6. Review + create​

The portal checks every tab, then shows your choices, the plan's price, and its terms. Choose Create.

The deployment page lists the cluster, if you asked for one, and then the extension. Azure waits until the relay pods are ready before it marks the extension Succeeded, so a finished deployment is a serving relay. If the deployment fails, see Troubleshooting.


After the install​

Check the inbound relay​

In the portal, open the cluster, then Kubernetes resources > Services and ingresses, and filter to the mte-relay-mar namespace. The mte-api-relay service's External IP is the inbound relay's address.

Or, from Azure Cloud Shell:

export RG=mte-relay # Update this value! The cluster's resource group
export CLUSTER=mte-relay # Update this value! The cluster's name

az aks get-credentials --resource-group $RG --name $CLUSTER
kubectl -n mte-relay-mar get pods
RELAY=$(kubectl -n mte-relay-mar get svc mte-api-relay -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$RELAY/api/mte-echo

Every pod is 1/1 Running, and the echo answers {"message":"true",...}.

Check the chain​

To try a full chain before the inbound relay has TLS, run the wizard a second time on the same cluster with these Relay tab values:

FieldValue
Relay roleOutbound
Extension name and Namespacemte-relay-mar-out
Inbound relay addresshttp://mte-api-relay.mte-relay-mar, the inbound Service's cluster name. With the inbound relay on Cluster only, add :8080.
Allow an http:// inbound addresson

Then call your API through the outbound relay, with the path and query your service would use:

kubectl -n mte-relay-mar-out run curl --rm -i --restart=Never --image=curlimages/curl:8.10.1 -- \
curl -s http://mte-api-relay.mte-relay-mar-out:8080/todos/1

The command prints your API's answer after the round trip through both relays. The inbound relay's log shows each request decrypted:

kubectl -n mte-relay-mar logs deployment/mte-api-relay | grep '"event_type":"decryption"'

Once the inbound relay is behind the gateway in Application Gateway for Containers, point the outbound relay at it:

az k8s-extension update --name mte-relay-mar-out --cluster-name $CLUSTER --resource-group $RG \
--cluster-type managedClusters --yes \
--config outbound.upstream=https://$RELAY_HOST outbound.allowPlainUpstream=false

The outbound pods restart with the new address. Run the curl again.

Call an API through the chain​

Point your service at the outbound relay instead of the destination API, and keep the path and query the same. No special request headers are needed.

# Before
curl -X GET https://api.partner.example.com/data

# After, from a pod in the outbound relay's cluster
curl -X GET http://mte-api-relay.mte-relay-mar-out:8080/data

With Cluster only, the outbound relay's Service listens on 8080; with an internal load balancer, on the load balancer port. Its route answers any host name your services use for it.

Open the inbound control plane​

You do not have to open the panel to start serving. Open it to add host names, change the API address, or tune limits.

On a machine with kubectl and a browser, after az aks get-credentials, start a port forward. It keeps running, so use a second terminal:

kubectl -n mte-relay-mar port-forward svc/mte-api-relay-control-plane 8081:8081

Open http://localhost:8081 and sign in as admin with the password from the Control plane tab. To read it back from the cluster:

kubectl -n mte-relay-mar get secret mte-api-relay-control-plane -o jsonpath='{.data.ADMIN_PW}' | base64 -d; echo

The panel opens on the dashboard with your first configuration in place. There is no setup wizard to run; that is the step the Marketplace form replaces.

  • Proxies holds one host, *, that sends every host name to your API, with a client secret the panel generated. Add a host such as partner-relay.example.com to route it somewhere else.
  • COMPANY_NAME and LICENSE_KEY under Configuration stay empty. The subscription is the license.
  • Configuration holds every other setting. Behind Application Gateway for Containers, set TRUSTED_PROXIES there, as in step 6.
  • Fleet lists each relay pod by name, with runtime kubernetes.

The forward is tied to one pod. When the control plane pod is replaced, it stops with lost connection to pod; run the same command again. The control plane guide covers every page of the panel. An outbound install has no panel: change its route with az k8s-extension update, as in Check the chain.


Production setup​

The wizard's defaults make a complete chain. For production, keep its shape and add these:

  • Each relay in its own cluster: the outbound relay with the calling services, the inbound relay with the API, each cluster on the Standard tier with availability zones and the cluster autoscaler on. A new cluster from the wizard has all three.
  • Two or more relay pods on each side, with autoscaling and a maximum you are willing to pay for.
  • Azure Managed Redis with a private endpoint for the inbound relay, as in Azure Managed Redis.
  • Application Gateway for Containers in front of the inbound relay, with a publicly trusted certificate, the web application firewall in Prevention mode, and TRUSTED_PROXIES set to its subnet, as in Application Gateway for Containers.
  • The outbound relay private: Cluster only, or an internal load balancer for callers in the virtual network.
  • The control plane reachable only privately: by kubectl port-forward, or by the internal load balancer from a VPN or a peered network. Never route port 8081 through the gateway.
  • A copy of the configuration outside the cluster: export it as in Backup and restore, or back the namespace up with Azure Backup for AKS.
  • Monitoring: Container insights for pod logs and node metrics, with alerts on each relay Deployment's available replicas. The relay writes JSON logs; see Monitoring and Integrations.

The rest of this section runs from bash, such as Azure Cloud Shell, with RG and CLUSTER set as in Check the inbound relay.

Azure Managed Redis​

For the inbound relay. Valkey in the cluster is one pod: if it restarts, the outbound relays pair again, and pairing requests in those seconds answer 503 state_store_unavailable. For a zone redundant store, use Azure Managed Redis. Microsoft is retiring Azure Cache for Redis and no longer creates new caches in the global Azure cloud.

The relay uses a single endpoint Redis client, so the cache needs the Enterprise or Non clustered clustering policy, never OSS, and access key authentication. Create it, give it a private endpoint in the cluster's virtual network, then turn public access off:

export LOCATION=westus3 # Update this value! The cluster's region
export AMR=mte-api-relay-redis-$RANDOM # Cache names are global

az extension add --name redisenterprise --upgrade
az redisenterprise create --resource-group $RG --name $AMR --location $LOCATION \
--sku Balanced_B1 --clustering-policy EnterpriseCluster --access-keys-auth Enabled \
--public-network-access Enabled

MC=$(az aks show -g $RG -n $CLUSTER --query nodeResourceGroup -o tsv)
VNET=$(az network vnet list -g $MC --query '[0].name' -o tsv)
az network vnet subnet create -g $MC --vnet-name $VNET -n snet-private-endpoints \
--address-prefixes 10.226.0.0/24
az network private-endpoint create -g $RG -n $AMR-pe --location $LOCATION \
--subnet $(az network vnet subnet show -g $MC --vnet-name $VNET -n snet-private-endpoints --query id -o tsv) \
--private-connection-resource-id $(az redisenterprise show -g $RG -n $AMR --query id -o tsv) \
--group-id redisEnterprise --connection-name $AMR-pe
az network private-dns zone create -g $RG -n privatelink.redis.azure.net
az network private-dns link vnet create -g $RG -z privatelink.redis.azure.net -n aks-vnet \
-v $(az network vnet show -g $MC -n $VNET --query id -o tsv) -e false
az network private-endpoint dns-zone-group create -g $RG --endpoint-name $AMR-pe -n default \
--private-dns-zone privatelink.redis.azure.net --zone-name redis
az redisenterprise update -g $RG -n $AMR --public-network-access Disabled
  • The commands assume the virtual network AKS created, 10.224.0.0/12. In your own virtual network, put the private endpoint in any subnet and link the zone to that network.
  • Balanced_B1 holds 1 GB. Pair state is small: each paired outbound relay keeps a few kilobytes per pair until it goes idle.

Point the inbound extension at the cache. A protected setting you leave out keeps its stored value, so send only the access key:

KEY=$(az redisenterprise database list-keys -g $RG --cluster-name $AMR --query primaryKey -o tsv)
az k8s-extension update --name mte-relay-mar --cluster-name $CLUSTER --resource-group $RG \
--cluster-type managedClusters --yes \
--config redis.mode=external redis.external.host=$AMR.$LOCATION.redis.azure.net \
redis.external.port=10000 redis.external.tls=true \
--config-protected redis.external.passwordBase64=$(printf '%s' "$KEY" | base64 -w0)
kubectl -n mte-relay-mar rollout status deployment/mte-api-relay
kubectl -n mte-relay-mar logs deployment/mte-api-relay | grep "Redis state store initialized"

The extension removes the Valkey pod, and each relay pod logs Redis state store initialized when it reaches the cache. To use the cache from the start, create it first and pick Existing Redis on the Relay tab.

Application Gateway for Containers​

For the inbound relay. The relay serves plain HTTP, and an outbound relay calls an https:// address, so put Application Gateway for Containers in front of the inbound relay: Azure's layer 7 load balancer for AKS, with TLS and Azure Web Application Firewall. These steps install its controller with Helm, the generally available path, and let the controller create the gateway.

Before you start:

  • The cluster must use Azure CNI, which includes Azure CNI Overlay, and be in a region where Application Gateway for Containers is available. A cluster the wizard created qualifies.
  • You need a certificate for the hostname outbound relays call, from a public certificate authority, in PEM files: the outbound relay checks it against the system trust store. The gateway reads it from a Kubernetes Secret: renew it by replacing the Secret, or have cert-manager manage it.
  • The commands use helm, which Azure Cloud Shell has.
export LOCATION=westus3 # Update this value!
export RELAY_HOST=partner-relay.example.com # Update this value! The hostname outbound relays call
export SUBNET_PREFIX=10.225.0.0/24 # Update this value! A free /24 in the cluster's virtual network

SUBNET_PREFIX must sit inside the cluster's virtual network and overlap no other subnet. The virtual network AKS creates is 10.224.0.0/12 with nodes in 10.224.0.0/16, so the example fits it.

1. Register the providers and enable workload identity​

for p in Microsoft.ContainerService Microsoft.Network Microsoft.NetworkFunction Microsoft.ServiceNetworking; do
az provider register --namespace $p --wait
done
az aks update -g $RG -n $CLUSTER --enable-oidc-issuer --enable-workload-identity

A cluster the wizard created already has both; the update returns at once.

2. Create the controller's identity​

MC=$(az aks show -g $RG -n $CLUSTER --query nodeResourceGroup -o tsv)
MC_ID=$(az group show -n $MC --query id -o tsv)
az identity create -g $RG -n azure-alb-identity
sleep 60
PRINCIPAL=$(az identity show -g $RG -n azure-alb-identity --query principalId -o tsv)
az role assignment create --assignee-object-id $PRINCIPAL --assignee-principal-type ServicePrincipal \
--scope $MC_ID --role acdd72a7-3385-48ef-bd42-f606fba81ae7
az role assignment create --assignee-object-id $PRINCIPAL --assignee-principal-type ServicePrincipal \
--scope $MC_ID --role fbc52c3f-28ad-4303-a892-8a056630b8f1
az identity federated-credential create --name azure-alb-identity --identity-name azure-alb-identity \
--resource-group $RG --issuer $(az aks show -g $RG -n $CLUSTER --query oidcIssuerProfile.issuerUrl -o tsv) \
--subject system:serviceaccount:azure-alb-system:alb-controller-sa

The roles are Reader and AppGw for Containers Configuration Manager on the cluster's node resource group. The pause lets the new identity replicate before the role assignments name it.

3. Install the controller​

helm install alb-controller oci://mcr.microsoft.com/application-lb/charts/alb-controller \
--namespace azure-alb-system --create-namespace --version 1.12.2 \
--set albController.namespace=azure-alb-system \
--set albController.podIdentity.clientID=$(az identity show -g $RG -n azure-alb-identity --query clientId -o tsv)
kubectl -n azure-alb-system rollout status deployment/alb-controller
kubectl get gatewayclass azure-alb-external

The gateway class reports ACCEPTED True.

4. Create the gateway's subnet​

VNET=$(az network vnet list -g $MC --query '[0].name' -o tsv)
az network vnet subnet create -g $MC --vnet-name $VNET -n subnet-alb \
--address-prefixes $SUBNET_PREFIX --delegations Microsoft.ServiceNetworking/trafficControllers
ALB_SUBNET_ID=$(az network vnet subnet show -g $MC --vnet-name $VNET -n subnet-alb --query id -o tsv)
az role assignment create --assignee-object-id $PRINCIPAL --assignee-principal-type ServicePrincipal \
--scope $ALB_SUBNET_ID --role 4d97b98b-1d4f-4787-a291-c67834d212e7

In your own virtual network, run the same commands with its resource group and name in place of $MC and $VNET. The role is Network Contributor on the subnet, which lets the controller join it.

5. Switch the relay to Cluster only and route to it​

If you installed with a load balancer, switch the inbound relay's Service to ClusterIP:

az k8s-extension update --name mte-relay-mar --cluster-name $CLUSTER --resource-group $RG \
--cluster-type managedClusters --yes \
--config service.exposure=cluster

Store your certificate and key for $RELAY_HOST, and create the gateway:

kubectl -n mte-relay-mar create secret tls mte-api-relay-tls --cert=relay.crt --key=relay.key

kubectl apply -f - <<EOF
apiVersion: alb.networking.azure.io/v1
kind: ApplicationLoadBalancer
metadata:
name: mte-api-relay
namespace: mte-relay-mar
spec:
associations:
- $ALB_SUBNET_ID
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: mte-api-relay
namespace: mte-relay-mar
annotations:
alb.networking.azure.io/alb-namespace: mte-relay-mar
alb.networking.azure.io/alb-name: mte-api-relay
spec:
gatewayClassName: azure-alb-external
listeners:
- name: https
port: 443
protocol: HTTPS
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: mte-api-relay-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mte-api-relay
namespace: mte-relay-mar
spec:
parentRefs:
- name: mte-api-relay
rules:
- backendRefs:
- name: mte-api-relay
port: 8080
EOF
kubectl -n mte-relay-mar wait --for=condition=Programmed gateway/mte-api-relay --timeout=15m
FQDN=$(kubectl -n mte-relay-mar get gateway mte-api-relay -o jsonpath='{.status.addresses[0].value}')
echo $FQDN
  • Azure creates the gateway in about six minutes. $FQDN ends in .alb.azure.com; point a CNAME record for $RELAY_HOST at it.
  • The gateway sends traffic only to ready relay pods: it takes its health check from the relay's readiness probe, GET /readyz on port 9091.
  • The gateway passes the original Host through, and the inbound relay routes by it.

6. Trust the gateway's forwarded addresses​

The relay sees the gateway's addresses, not the outbound relays'. In the panel, open Configuration and set TRUSTED_PROXIES to the subnet's CIDR, the value of $SUBNET_PREFIX (10.225.0.0/24 in the example). Save, then restart the relay pods so each outbound relay gets its own rate limit budget again:

kubectl -n mte-relay-mar rollout restart deployment/mte-api-relay

See Behind a load balancer.

7. Attach the web application firewall​

az network application-gateway waf-policy create -g $RG -n mte-api-relay-waf -l $LOCATION \
--type Microsoft_DefaultRuleSet --version 2.1
az network application-gateway waf-policy policy-setting update -g $RG --policy-name mte-api-relay-waf \
--mode Prevention --state Enabled --request-body-check false
WAF_ID=$(az network application-gateway waf-policy show -g $RG -n mte-api-relay-waf --query id -o tsv)
az role assignment create --assignee-object-id $PRINCIPAL --assignee-principal-type ServicePrincipal \
--scope $WAF_ID --role 4d97b98b-1d4f-4787-a291-c67834d212e7

kubectl apply -f - <<EOF
apiVersion: alb.networking.azure.io/v1
kind: WebApplicationFirewallPolicy
metadata:
name: mte-api-relay
namespace: mte-relay-mar
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: mte-api-relay
namespace: mte-relay-mar
webApplicationFirewall:
id: $WAF_ID
EOF
kubectl -n mte-relay-mar wait --for=condition=Programmed webapplicationfirewallpolicy/mte-api-relay --timeout=5m
  • Application Gateway for Containers supports the Default Rule Set 2.1.
  • --request-body-check false is required. Relay bodies are MTE ciphertext or key material: with body inspection on, the rule set blocks every pairing request, POST /api/mte-pair. The firewall still inspects URLs, query strings, and headers.
  • The controller's identity needs Network Contributor on the policy to join it. Without it the policy stays unprogrammed with LinkedAuthorizationFailed.
  • To admit only known callers, add a custom rule that allows the outbound relays' egress addresses and blocks the rest.

8. Check it​

curl https://$RELAY_HOST/api/mte-echo
curl -s -o /dev/null -w '%{http_code}\n' "https://$RELAY_HOST/api/mte-echo?q=/etc/passwd"

The echo answers through TLS, and the second request gets 403 from the firewall. Before DNS is in place, add --connect-to $RELAY_HOST:443:$FQDN:443 to both commands. Then point the outbound relay at https://$RELAY_HOST as in Check the chain and run its curl again.


Change settings later​

Every field on the wizard's tabs is an extension setting, listed in Settings reference. Change one with az k8s-extension update; it sends only the keys you name:

az k8s-extension update --name mte-relay-mar --cluster-name $CLUSTER --resource-group $RG \
--cluster-type managedClusters --yes \
--config relay.autoscaling.minReplicas=3 relay.autoscaling.maxReplicas=10
  • Never kubectl scale or kubectl edit the relay. The extension reapplies its own values on every update and automatic upgrade.

  • The inbound API address from the Relay tab seeds the first configuration only. After the install, change it in the panel under Proxies; updating controlPlane.initialConfig.upstream has no effect.

  • The outbound route is an extension setting: update outbound.upstream with az k8s-extension update, and the outbound pods restart with the new address.

  • Protected settings, the admin password and the Redis access key, travel base64 encoded with --config-protected. A protected setting you leave out keeps its stored value.

  • The node range lives on the node pool. A cluster the wizard created names it relay:

    az aks nodepool update --resource-group $RG --cluster-name $CLUSTER --name relay \
    --update-cluster-autoscaler --min-count 2 --max-count 8

    On your own cluster, turn the autoscaler on for the pool the relay runs in with --enable-cluster-autoscaler instead.

  • Upgrades: with automatic upgrades on, Azure installs new 5.x versions. To pin a version, run az k8s-extension update ... --yes --auto-upgrade-mode none, then move with --version. Upgrade the inbound relay first.

  • Restart fields: some panel settings apply at the next restart; the panel says which. Run kubectl -n mte-relay-mar rollout restart deployment/mte-api-relay.

  • Updates keep serving: relay pods roll one at a time, each new pod joins only when ready, and each old one drains for up to 30 seconds.

  • The control plane is down: inbound relay pods keep serving their last configuration. An inbound relay pod that starts during the outage stays 0/1 until the panel answers.

  • Outbound pair pools: each outbound relay pod sizes its pair pool to demand, up to 1024 pairs, and keeps it in memory.

  • Uninstall: in the portal, open the cluster's Extensions + applications, select the extension, and choose Uninstall, or run az k8s-extension delete. Billing stops with it. Uninstalling removes what the extension created, including an inbound install's disk, so export the configuration first if you may reinstall.

Settings reference​

KeyWizard fieldDefaultMeaning
roleRelay roleinboundinbound or outbound.
controlPlane.initialConfig.upstreamAPI addressemptyInbound: the first configuration's API. Required for an inbound install to finish.
outbound.upstreamInbound relay addressemptyOutbound: the inbound relay's address. Required for an outbound install.
outbound.allowPlainUpstreamAllow an http:// inbound addressfalseOutbound: allows an http:// address.
outbound.hostnone*Outbound: the Host the route answers.
service.exposureExpose the relay throughpublicpublic, internal, or cluster. An outbound relay refuses public.
service.portLoad balancer port80The load balancer's port.
service.allowedSourceRangesAllowed client address rangesemptyCIDRs separated by spaces, since settings values cannot hold commas.
service.dnsLabelDNS name labelemptyPublic DNS name label.
service.publicIpName, service.publicIpResourceGroupnoneemptyAn existing Standard public IP for the load balancer.
service.internalSubnetnoneemptyThe subnet for an internal load balancer.
service.idleTimeoutMinutesnone30The load balancer's TCP idle timeout, 4 to 100.
redis.modeShared pair statenoneInbound: none, inCluster, or external.
redis.external.host, redis.external.port, redis.external.tlsRedis host name, port, Connect with TLSempty, 10000, trueAn external Redis.
relay.autoscaling.enabledScale relay pods on CPUfalseTurns the horizontal pod autoscaler on.
relay.replicasRelay pods, minimum1Relay pods when autoscaling is off.
relay.autoscaling.minReplicas, relay.autoscaling.maxReplicasRelay pods, minimum and maximum2, 6The autoscaler's range.
relay.autoscaling.targetCPUUtilizationPercentageTarget average CPU80The CPU target.
relay.resources.requests.cpu, .memory, relay.resources.limits.cpu, .memoryRelay pod size1, 1Gi, 2, 2GiEach relay pod's CPU and memory. Set relay.goMemLimit to about 85 percent of the memory limit.
controlPlane.accessReach the control plane throughportForwardportForward or internal.
controlPlane.allowedSourceRangesAllowed administrator address rangesemptyCIDRs separated by spaces.
controlPlane.internalSubnetnoneemptyThe subnet for the panel's internal load balancer.
controlPlane.storage.size, controlPlane.storage.storageClassSettings disk1Gi, managed-csiThe panel's disk. It can grow, not shrink.
metrics.enabled, metrics.portExpose Prometheus metricsfalse, 9090Prometheus /metrics.
networkPolicy.enablednonetrueThe network policies for the panel and Valkey.

The defaults are the chart's own, for an install from the command line; the wizard sets its own defaults on every key it shows. The protected settings are controlPlane.adminPasswordBase64 and redis.external.passwordBase64, each base64 encoded.

Install with the Azure CLI​

The same installs without the portal, from bash:

az provider register --namespace Microsoft.KubernetesConfiguration --wait
az extension add --name k8s-extension --upgrade
az vm image terms accept --publisher eclypses1708534446098 \
--offer eclypses_mte_api_relay --plan eclypse_mte_api_relay

ADMIN_PW=$(openssl rand -base64 24)
echo "Admin password: $ADMIN_PW" # Store it in your password manager.

# Inbound, in front of your API
az k8s-extension create --name mte-relay-mar --extension-type eclypses.mte-api-relay \
--cluster-name $CLUSTER --resource-group $RG --cluster-type managedClusters \
--scope namespace --target-namespace mte-relay-mar \
--plan-name eclypse_mte_api_relay --plan-product eclypses_mte_api_relay \
--plan-publisher eclypses1708534446098 --release-train stable \
--config role=inbound controlPlane.initialConfig.upstream=https://api.example.com \
service.exposure=public redis.mode=inCluster \
relay.autoscaling.enabled=true relay.autoscaling.minReplicas=2 relay.autoscaling.maxReplicas=6 \
--config-protected controlPlane.adminPasswordBase64=$(printf '%s' "$ADMIN_PW" | base64 -w0)

# Outbound, beside the calling services
az k8s-extension create --name mte-relay-mar-out --extension-type eclypses.mte-api-relay \
--cluster-name $CLUSTER --resource-group $RG --cluster-type managedClusters \
--scope namespace --target-namespace mte-relay-mar-out \
--plan-name eclypse_mte_api_relay --plan-product eclypses_mte_api_relay \
--plan-publisher eclypses1708534446098 --release-train stable \
--config role=outbound outbound.upstream=https://partner-relay.example.com \
service.exposure=cluster relay.autoscaling.enabled=true relay.autoscaling.minReplicas=2 relay.autoscaling.maxReplicas=6
  • The plan id is eclypse_mte_api_relay, as published.
  • az vm image terms accept accepts the plan's terms once per subscription; the portal does it for you.
  • Without controlPlane.initialConfig.upstream the inbound relay pods wait for a configuration that never comes, and Azure fails the install when its wait runs out.
  • Protected settings travel base64 encoded so any character in a password survives, and a value that is not base64 fails the install. On macOS, use base64 without -w0.

Without the Marketplace​

With an Eclypses license instead of the Marketplace plan, run the on premise image on AKS with the Kubernetes walkthrough there. On AKS, keep securityContext.fsGroup: 65532 on the control plane, or a root owned Azure Disk blocks it from writing its data directory, and use the service.beta.kubernetes.io/azure-load-balancer-internal annotation for a private load balancer.


What the install runs​

kubectl -n mte-relay-mar get deploy,svc,hpa,pdb,pvc,secret,networkpolicy
ObjectNamePurpose
Deploymentmte-api-relayThe relay pods.
Deploymentmte-api-relay-control-planeInbound only: one control plane pod.
Deployment and Servicemte-api-relay-redisInbound only: one Valkey pod and its port, with Valkey in the cluster.
Servicemte-api-relayThe relay's load balancer, or a ClusterIP on port 8080 with Cluster only.
Servicemte-api-relay-control-planeInbound only: the panel on port 8081.
HorizontalPodAutoscalermte-api-relayWith autoscaling on.
PodDisruptionBudgetmte-api-relayWhen the minimum is two or more relay pods: keeps all but one serving while a node drains.
PersistentVolumeClaimmte-api-relay-control-plane-dataInbound only: the panel's Azure Disk.
Secretmte-api-relay-control-planeInbound only: the admin password and the token relay pods use to reach the panel.
Secretmte-api-relay-redisInbound only: the Redis URL the relay pods use, and the Valkey or cache password.
Secretmte-api-relay-metricsThe bearer token for /metrics, with metrics on.
NetworkPolicymte-api-relay-control-plane, mte-api-relay-redisInbound only: admit only relay pods to Valkey and to the panel. With the internal load balancer or metrics on, ports 8081 and 9090 also admit the cluster's network.

Every pod runs as a non root user with a read only root filesystem, under the restricted Pod Security Standard, and only on Linux amd64 nodes. Clusters with Azure CNI powered by Cilium, as the wizard creates, or another policy engine enforce the network policies.


Server Configuration​

Routing lives in DOMAIN_MAP. The inbound relay takes it from its control plane; the outbound install builds a single entry with outbound: true whose upstream is the inbound relay. The on premise guide walks through both.


Testing and Health Checks​

  • GET /api/mte-echo and HEAD /api/mte-echo on port 8080 answer for any Host, outside every rate limit, with {"message":"true","timestamp":"2026-01-04T10:30:45.123456789Z"}; ?msg=test makes "message":"test", always a JSON string. Any other method gets 405 with Allow: GET, HEAD.
  • GET /healthz on port 9091 answers 200 {"status":"ok","phase":"<phase>"} for as long as the process runs. GET /readyz on port 9091 answers 200 {"status":"ready","phase":"ready"} while the relay takes traffic, or 503 {"status":"not_ready","phase":"<phase>","reason":"<code>"} otherwise, with reason only when there is one. Both take GET and HEAD. The extension points the pods' readiness and liveness probes at them.
  • Health check path for scanners and load balancers: HEALTH_CHECK_PATH, off by default, mounts a health answer on port 8080 at a path you pick, for example /health. GET and HEAD there answer 200 {"status":"ready"} while the relay serves and 503 {"status":"not_ready"} once shutdown begins, for any Host and outside every rate limit. See Health checks and scanners.
  • A request through the outbound relay reaches the destination API, as in Check the chain.

Every probe, echo, and health check path answer carries Content-Type: application/json, Cache-Control: no-store, no-transform, private, and X-Content-Type-Options: nosniff; the echo adds X-MTE-Relay-Request-Id.


Monitoring​

  • Enable Container insights to collect pod logs and node and pod metrics: az aks enable-addons -a monitoring -g $RG -n $CLUSTER.
  • MTE API Relay v5 writes structured JSON logs. The dashboards and alert rules under Monitoring and Integrations apply once your log collector ships them.
  • With Expose Prometheus metrics on, scrape port 9090 on each relay pod and the panel with the bearer token from kubectl -n mte-relay-mar get secret mte-api-relay-metrics -o jsonpath='{.data.METRICS_TOKEN}' | base64 -d. Port 9090 is not on the Services, so scrape the pods.

Troubleshooting​

SymptomCause and fix
The Cluster tab shows "The value must not be empty" under Kubernetes version, and Review + create fails validation.The version list starts with nothing selected. Pick the first version in the list.
The Cluster tab says the resource group has no AKS cluster.Basics names a different resource group. Choose the one the cluster lives in, or choose Yes to create a cluster.
The Relay tab says "An http:// address needs Allow an http:// inbound address".Tick Allow an http:// inbound address, for an inbound relay on a private network only, or use the inbound relay's https:// address.
The deployment fails with a Marketplace purchase or payment instrument error.The subscription cannot buy Marketplace software, or the plan's terms are not accepted. Use a pay as you go or Enterprise Agreement subscription, and accept the terms in the portal or with az vm image terms accept.
The extension shows Failed with "Timed out waiting for the resource to come to a ready/completed state".Relay pods never turned ready. Run kubectl -n mte-relay-mar get pods and read the log of any pod that is not 1/1. An inbound install from the command line without controlPlane.initialConfig.upstream always ends this way.
Inbound relay pods stay 0/1, and /readyz reports waiting_for_configuration.The control plane holds no configuration. Open the panel and add a host under Proxies.
Inbound relay pods stay 0/1 with waiting_for_control_plane.The control plane pod is not running. Check kubectl -n mte-relay-mar get pods and its log.
The extension shows Failed with an error about two or more relay pods.More than one inbound relay pod needs shared state. Set redis.mode to inCluster or external.
The extension shows Failed saying an outbound relay is never public.Set service.exposure to cluster or internal.
The extension shows Failed naming outbound.upstream.The inbound address is missing, or it is http:// without outbound.allowPlainUpstream=true.
The extension shows Failed saying a setting must be base64 encoded.A protected setting was sent as plain text. Send $(printf '%s' "$VALUE" | base64 -w0).
az k8s-extension update stops at Do you wish to proceed? or rejects --plan-name.Add --yes; only create takes the plan flags.
480 no_domain_for_host from the inbound relay.No host in its configuration matches the hostname the outbound relay calls. This happens once the * host is removed. Add the host under Proxies.
502 with a cause from the outbound relay.The inbound relay or the API failed; cause holds the inbound relay's code. A TLS error means the gateway's certificate is not publicly trusted or does not match the hostname.
The outbound relay gets 403 from the gateway on POST /api/mte-pair.The firewall inspects request bodies. Run az network application-gateway waf-policy policy-setting update ... --request-body-check false.
The WebApplicationFirewallPolicy stays unprogrammed with LinkedAuthorizationFailed.The controller's identity lacks Network Contributor on the policy. Assign it as in step 7.
Pairing answers 503 state_store_unavailable.The inbound relay cannot reach Redis. With Valkey, wait for its pod to restart. With Azure Managed Redis, check the private endpoint, the DNS zone link, and the access key.
Inbound relay pods log NOAUTH or WRONGPASS.The access key is wrong, or access key authentication is off on the cache. Update redis.external.passwordBase64.
Relay pods log CROSSSLOT or MOVED.The cache uses the OSS clustering policy. Create it with EnterpriseCluster or NoCluster.
The load balancer's address stays <pending>.The DNS label is taken in the region, or a static IP name is wrong. Check kubectl -n mte-relay-mar describe svc mte-api-relay.

Set LOG_LEVEL to debug under Configuration for more detail, and see Errors and recovery.


Security​

Container Hardening​

  • Minimal base image. The runtime image uses Google's Distroless cc-debian13 base, with no shell, package manager, or general purpose OS utilities.
  • Multi stage build. Build tooling and source never ship in the final image; only the compiled binary is copied forward.
  • Unprivileged runtime. Every pod runs as a non root user, with a read only root filesystem, no added capabilities, and no service account token.
  • Small listening surface. The image declares ports 8080, 9091, and 9090. The probe listener on 9091 serves the kubelet, and the Prometheus listener opens only with metrics on.

Secrets and network​

  • The admin password, the relay pods' token, and the Redis password live in Kubernetes Secrets in the namespace. Restrict who can read Secrets there.
  • The panel holds the client secrets and, with Redis, the Redis URL. Keep port 8081 private, and see Security on the control plane page.

Vulnerability Scanning​

Microsoft scans every Marketplace version before it publishes it and blocks images with findings of CVSS 7 or higher. Scan reports for the current version are available from Eclypses Support on request (see Support).

You can also generate your own scan evidence with Microsoft Defender for Containers on the cluster.


Costs​

Azure ServicePurpose
Azure MarketplaceThe plan's hourly price for every running relay pod and each inbound control plane pod
Azure Kubernetes ServiceThe clusters, their Standard tier, and the node virtual machines
Azure Managed DisksThe inbound control plane's settings
Azure Load Balancer, or Application Gateway for ContainersTraffic to the inbound relay pods, with TLS on the gateway
Azure Web Application FirewallRequest filtering on the gateway
Azure Managed RedisShared pair state for two or more inbound relay pods
Azure MonitorLogging and monitoring

Maintenance​

Release Cadence​

Updated versions are published on an as needed basis, when new features, dependency updates, or fixes warrant a release. Security patches are issued out of band as needed and are not tied to a fixed calendar cadence. With automatic upgrades on, Azure installs each new 5.x version; the current release is 5.0.5.

Patch SLA​

Eclypses' commitment for security patches:

  • Critical vulnerabilities are addressed in a patched image within 30 days of confirmed disclosure.
  • Lower severity issues are addressed in a subsequent release.

Fault Recovery​

Relay pods that fail their liveness probe are restarted, and pods on a failed node are rescheduled. With Redis, the outbound relays keep their pairs across inbound relay pod restarts. When the inbound relay loses them, without Redis or when Valkey restarts, the request that finds its pair gone fails back to your service, and the outbound relay pairs again before the next one.

Service Limits​

Secret Rotation​

Rotate each inbound entry's client_id_secret under Proxies on a schedule that fits your security policy. After a change, the outbound relay authenticates and pairs again.


Support​

MTE API Relay is a commercially supported product from Eclypses. Support and security patch SLAs (including the 30 day critical patch commitment described under Maintenance) are provided to licensed and subscribed customers. For escalations, scan reports, or hardening documentation, contact Eclypses Support: