Skip to main content
Version: 5.x

MTE Relay Server on Azure

Introduction​

MTE Relay Server is a proxy that runs in front of your backend. Client applications use an MTE Relay client library to send encrypted requests; the relay decrypts each one, forwards the plaintext request to your backend, and encrypts the response on the way back.

Example MTE Relay Server diagram

info

MTE Relay Server pairs only with the MTE Relay client libraries. It cannot pair with an MTE API Relay. v5 relays serve only v5 clients; see Upgrading from v4.

On Azure, MTE Relay Server is a Kubernetes app on Azure Marketplace. You install it with a six tab wizard in the Azure portal, and the wizard does the rest: it creates an Azure Kubernetes Service (AKS) cluster or uses one you have, installs the relay as a cluster extension, and stores your backend's address as the relay's first configuration. When the deployment finishes, the relay is already forwarding requests. One namespace holds everything:

  • The relay pods, scaled by a horizontal pod autoscaler on CPU.
  • The control plane, the web panel that configures every relay pod, with its settings on an Azure Disk.
  • Optionally, one Valkey pod that holds the pair state the relay pods share.

The offer bills through your Azure subscription at the plan's hourly price for every running relay pod and for the control plane pod, 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, so one relay pod with its control plane bills two pods, and the default range of two to six relay pods bills three to seven.

For MTE Relay v4 on Azure, see the MTE 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.
  • Your backend's address, such as https://api.example.com. The wizard asks for it, and the relay must be able to reach it from the cluster.
  • The origins of any web apps that will call the relay from a browser, such as https://app.example.com. Mobile and server clients need none.
  • An admin password for the control plane, 16 characters or more, kept in your password manager.
  • An AKS cluster, 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 your backend and, with Azure Managed Redis, port 10000 to the cache.


Install from Azure Marketplace​

In the Azure portal, open Marketplace, search for MTE Relay Server, 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.

The wizard has six tabs. Each one is below with its defaults; most installs change only the fields marked here as required.

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, 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​

The Relay tab has three sections. Only Backend API address is required.

Backend

Relay tab, Backend section: backend API address, allowed browser origins, extension name, and namespace

FieldWhat to enter
Backend API addressRequired. Where the relay forwards decrypted requests, such as https://api.example.com. Use http:// only for a backend on a private network. The address may carry a path but no commas or spaces.
Allowed browser originsThe origins of the web apps that call the relay from a browser, separated by commas, such as https://app.example.com. *.example.com or https://*.example.com allows every subdomain. Leave it empty for mobile and server clients.
Extension nameDefaults to mte-relay-mrs. The extension's name on the cluster. Each install in a cluster needs its own.
NamespaceDefaults to mte-relay-mrs. Use a new namespace; one install per namespace. The commands on this page assume the default.

The address becomes a single route that matches every host name: whatever host a client calls, the relay forwards to this backend. Add host specific routes later in the control plane, under Proxies.

A lone * in Allowed browser origins lets every website call the relay from its visitors' browsers, with their cookies. The tab warns when you enter it. List your app origins instead.

Network

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 clients 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 203.0.113.0/24.
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. A public load balancer is the quick way to try the relay. 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.

Scaling

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 client. 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, clients pair again on their next request. 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.

4. Control plane​

Control plane tab: 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-relay-metrics.

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. Turn it off to stay on this version until you upgrade with az k8s-extension update.
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 relay​

In the portal, open the cluster, then Kubernetes resources > Services and ingresses, and filter to the mte-relay-mrs namespace. The mte-relay service's External IP is the 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-mrs get pods
RELAY=$(kubectl -n mte-relay-mrs get svc mte-relay -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$RELAY/api/mte-echo
curl "http://$RELAY/api/mte-relay?frameVersions=2"

Every pod is 1/1 Running, the echo answers {"message":"true",...}, and the discovery call returns a client id with "buildVersion":"5.0.5". On an internal load balancer, run the same commands from a machine in the virtual network. With Cluster only, forward the relay's port and call localhost:8080 instead:

kubectl -n mte-relay-mrs port-forward svc/mte-relay 8080:8080

Connect your app​

Point the client library at the relay's address, http://<external IP> while you test, or your own https:// host name once the relay sits behind Application Gateway for Containers:

A browser app needs its origin under Allowed browser origins, or under Allowed origins for the host in the control plane, or its requests fail with a CORS error.

Open the control plane​

You do not have to open the panel to start serving. Open it to add host names, change the backend, set CORS, 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-mrs port-forward svc/mte-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-mrs get secret mte-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 backend, with the browser origins you entered and a client secret the panel generated. Add a host such as relay.example.com to route it somewhere else, or to give it its own CORS settings.
  • 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.


Production setup​

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

  • Standard tier and availability zones on the cluster, with the relay node pool in every zone and the cluster autoscaler on. A new cluster from the wizard has all three.
  • Two or more relay pods with autoscaling, and a maximum you are willing to pay for.
  • Azure Managed Redis with a private endpoint, as in Azure Managed Redis.
  • Application Gateway for Containers 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 relay's Service stays ClusterIP.
  • 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 the 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 relay.

Azure Managed Redis​

Valkey in the cluster is one pod: if it restarts, clients 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-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 client keeps a few kilobytes per pair until it goes idle.

Point the 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-mrs --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-mrs rollout status deployment/mte-relay
kubectl -n mte-relay-mrs logs deployment/mte-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​

The relay serves plain HTTP. For production, put Application Gateway for Containers in front of it: 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 clients call, from a public certificate authority, in PEM files. 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=relay.example.com # Update this value! The hostname clients 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 relay's Service to ClusterIP:

az k8s-extension update --name mte-relay-mrs --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-mrs create secret tls mte-relay-tls --cert=relay.crt --key=relay.key

kubectl apply -f - <<EOF
apiVersion: alb.networking.azure.io/v1
kind: ApplicationLoadBalancer
metadata:
name: mte-relay
namespace: mte-relay-mrs
spec:
associations:
- $ALB_SUBNET_ID
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: mte-relay
namespace: mte-relay-mrs
annotations:
alb.networking.azure.io/alb-namespace: mte-relay-mrs
alb.networking.azure.io/alb-name: mte-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-relay-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mte-relay
namespace: mte-relay-mrs
spec:
parentRefs:
- name: mte-relay
rules:
- backendRefs:
- name: mte-relay
port: 8080
EOF
kubectl -n mte-relay-mrs wait --for=condition=Programmed gateway/mte-relay --timeout=15m
FQDN=$(kubectl -n mte-relay-mrs get gateway mte-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 listener has no hostname, so every name reaches the relay, and the relay routes by Host.

6. Trust the gateway's forwarded addresses​

The relay sees the gateway's addresses, not the clients'. 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 client gets its own rate limit budget again:

kubectl -n mte-relay-mrs rollout restart deployment/mte-relay

See Behind a load balancer.

7. Attach the web application firewall​

az network application-gateway waf-policy create -g $RG -n mte-relay-waf -l $LOCATION \
--type Microsoft_DefaultRuleSet --version 2.1
az network application-gateway waf-policy policy-setting update -g $RG --policy-name mte-relay-waf \
--mode Prevention --state Enabled --request-body-check false
WAF_ID=$(az network application-gateway waf-policy show -g $RG -n mte-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-relay
namespace: mte-relay-mrs
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: mte-relay
namespace: mte-relay-mrs
webApplicationFirewall:
id: $WAF_ID
EOF
kubectl -n mte-relay-mrs wait --for=condition=Programmed webapplicationfirewallpolicy/mte-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, and browsers report the 403 as a CORS error. 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.

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.


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-mrs --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 backend address and browser origins from the Relay tab seed the first configuration only. After the install, change them in the panel under Proxies; updating controlPlane.initialConfig.* has no effect.

  • 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.

  • Restart fields: some panel settings apply at the next restart; the panel says which. Run kubectl -n mte-relay-mrs rollout restart deployment/mte-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 after the load balancer stops sending it traffic.

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

  • 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 the panel's disk, so export the configuration first if you may reinstall.

In Eclypses' test of this chart on AKS, load generators held about 17,600 requests a second against the echo endpoint for six minutes. The autoscaler grew the relay from two pods to its maximum of six, the cluster autoscaler added a node within 90 seconds, and a settings update replaced every pod during the run. All 6.3 million requests answered 200.

Settings reference​

KeyWizard fieldDefaultMeaning
controlPlane.initialConfig.upstreamBackend API addressemptyThe first configuration's backend. Required for an install to finish.
controlPlane.initialConfig.corsOriginsAllowed browser originsemptyOrigins separated by spaces, since settings values cannot hold commas.
service.exposureExpose the relay throughpublicpublic, internal, or cluster.
service.portLoad balancer port80The load balancer's port.
service.allowedSourceRangesAllowed client address rangesemptyCIDRs separated by spaces.
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 statenonenone, 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 install 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_relay_server --plan eclypses_mte_relay_server

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

az k8s-extension create --name mte-relay-mrs --extension-type eclypses.relay-server \
--cluster-name $CLUSTER --resource-group $RG --cluster-type managedClusters \
--scope namespace --target-namespace mte-relay-mrs \
--plan-name eclypses_mte_relay_server --plan-product eclypses_mte_relay_server \
--plan-publisher eclypses1708534446098 --release-train stable \
--config controlPlane.initialConfig.upstream=https://api.example.com \
controlPlane.initialConfig.corsOrigins=https://app.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)
  • az vm image terms accept accepts the plan's terms once per subscription; the portal does it for you.
  • Without controlPlane.initialConfig.upstream the 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-mrs get deploy,svc,hpa,pdb,pvc,secret,networkpolicy
ObjectNamePurpose
Deploymentmte-relayThe relay pods.
Deploymentmte-relay-control-planeOne control plane pod.
Deployment and Servicemte-relay-redisOne Valkey pod and its port, with Valkey in the cluster.
Servicemte-relayThe relay's load balancer, or a ClusterIP on port 8080 with Cluster only.
Servicemte-relay-control-planeThe panel on port 8081.
HorizontalPodAutoscalermte-relayWith autoscaling on.
PodDisruptionBudgetmte-relayWhen the minimum is two or more relay pods: keeps all but one serving while a node drains.
PersistentVolumeClaimmte-relay-control-plane-dataThe panel's Azure Disk.
Secretmte-relay-control-planeThe admin password and the token relay pods use to reach the panel.
Secretmte-relay-redisThe Redis URL the relay pods use, and the Valkey or cache password.
Secretmte-relay-metricsThe bearer token for /metrics, with metrics on.
NetworkPolicymte-relay-control-plane, mte-relay-redisAdmit 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​

MTE Relay Server is configured with environment variables. On Azure, the control plane holds the configuration and every relay pod reads it from there; see the control plane.


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. Set it under Configuration for a security scanner that can only reach the relay port. See Health checks and scanners.
  • GET /api/mte-relay?frameVersions=2 returns a client id and the discovery document for any Host while the * route from the install exists, and for each host under Proxies after that.

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 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-mrs get secret mte-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 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-mrs get pods and read the log of any pod that is not 1/1. An install from the command line without controlPlane.initialConfig.upstream always ends this way.
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.
Relay pods stay 0/1 with waiting_for_control_plane.The control plane pod is not running. Check kubectl -n mte-relay-mrs get pods and its log.
The extension shows Failed with an error about two or more relay pods.More than one relay pod needs shared state. Set redis.mode to inCluster or external.
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.No host in the configuration matches the Host the client sent. This happens once the * host is removed. Add the host under Proxies.
Browsers report a CORS error.The app's origin is not allowed. Add it under Allowed origins for the host under Proxies. Behind the gateway, also check the firewall row below.
Browsers report a CORS error and the gateway answers 403 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 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.
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-mrs describe svc mte-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 the control plane pod
Azure Kubernetes ServiceThe cluster, its Standard tier, and the node virtual machines
Azure Managed DisksThe control plane's settings
Azure Load Balancer, or Application Gateway for ContainersTraffic to the relay pods, with TLS on the gateway
Azure Web Application FirewallRequest filtering on the gateway
Azure Managed RedisShared pair state for two or more 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, clients keep their pairs across relay pod restarts. Without it, or when Valkey restarts, clients pair again: the browser engine does this when a request gets 475 invalid_client_id, but that request still fails with an MteRelayError marked repaired, and your code decides whether to send it again.

Service Limits​

Secret Rotation​

Rotate each host's client_id_secret under Proxies on a schedule that fits your security policy. After a change, clients authenticate and pair again.


Support​

MTE Relay Server 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: