Skip to content

Application Gateways ​

An Application Gateway puts one public address in front of many services in your VPC and routes each request to the right one by hostname and URL path. It terminates TLS, can re-encrypt to your backends, and applies a network firewall — source CIDR rules, country blocking and rate limits — before a request ever reaches your application.

It is a managed appliance: a dedicated instance in your own VPC, with a size, a version, and its own place in your bill.

Application Gateway or Load Balancer? ​

Both accept public traffic. They answer different questions.

Load BalancerApplication Gateway
What it doesSpreads traffic across a pool of backendsDecides where each request goes, and whether it may go at all
RoutingOne service per listener portMany services behind one address, by host and path
Addresses neededOne per load balancerOne, for all the services behind it
Request-level rules—Path rewriting, header injection, per-route backends
ProtectionAllowed CIDRs on a listenerCIDR rules, country blocking, rate limits, connection caps

The short version: if you want distribution, use a Load Balancer. If you want policy, use an Application Gateway. They compose — a gateway backend pool can point at a load balancer's address.

Create a gateway ​

A gateway lives in one VPC and one subnet, and needs a public address. Either let the platform allocate one, or bring one you already hold.

bash
# Platform-allocated address
fm appgw create --name edge --flavor agw.co1.medium \
  --vpc <vpc-id> --subnet <subnet-id>

# Bring your own Public IP
fm appgw create --name edge --flavor agw.co1.medium \
  --vpc <vpc-id> --subnet <subnet-id> \
  --public-ip-mode selected --public-ip <public-ip-id>

The two modes differ in one way that matters later: an address the platform allocated is released when you delete the gateway; an address you brought is not. It returns to your Public IPs and stays yours.

Sizes come from the catalog:

bash
fm appgw flavors

A size describes what the appliance carries, not the machine underneath it: rated throughput, the concurrent connections it holds, and how many listeners, routes, backends and firewall rules it will accept.

Those four limits are enforced — exceeding one is refused, not throttled — so pick a size for the ruleset you intend to write, not only for the traffic. The request rate is a rating rather than a guarantee: what you actually get depends on your rules, your TLS settings and your backends.

You do not choose an appliance version. Frostmoln manages it: you author routes, backends and WAF rules, never the data-plane configuration underneath them. The version-shaped choice you do control is your WAF ruleset, which has its own versions, publishing and rollback.

In Terraform the resource is frostmoln_application_gateway, with frostmoln_appgw_listener, frostmoln_appgw_route, frostmoln_appgw_backend_pool, frostmoln_appgw_backend, frostmoln_appgw_health_check and frostmoln_appgw_certificate hanging off it. A TCP listener is protocol = "tcp" with backend_pool_id, and optionally port_range_end; PROXY protocol is proxy_protocol on the pool, and on the health check for its probe connection.

Listeners — where traffic arrives ​

A listener binds a protocol and port. HTTPS listeners terminate TLS, so they need a certificate.

bash
fm appgw certificate upload --gateway <gw-id> --name example-com \
  --chain ./fullchain.pem --private-key ./privkey.pem

fm appgw listener create --gateway <gw-id> --name https \
  --protocol https --port 443 --certificate <certificate-id> \
  --tls-min-version 1.2 --tls-cipher-profile modern

Pass --private-key a path, or - to read from standard input. Giving the key text directly puts it in your shell history and in the process list.

Serving several hostnames from one listener? Add more certificates with --sni-certificate (repeatable); the right one is chosen per connection.

A plain HTTP listener can simply send everyone to HTTPS:

bash
fm appgw listener create --gateway <gw-id> --name http \
  --protocol http --port 80 --redirect-to-https

TCP listeners — when the traffic is not HTTP ​

Some things you need to publish do not speak HTTP: a mail server, an MQTT broker for a fleet of devices, a database replica, a game server. A TCP listener carries those through the gateway without reading them.

bash
fm appgw listener create --gateway <gw-id> --name smtp \
  --protocol tcp --port 25 --backend-pool <pool-id>

It is a different shape from an HTTP listener, and the differences are refusals, not omissions — the platform says no rather than accepting a setting and quietly ignoring it:

  • It names one backend pool. A TCP listener has no routes, because routing means matching a hostname, a path or a header, and none of that exists in a stream of bytes. --backend-pool is required here, and refused on an HTTP or HTTPS listener where each route names its own pool.
  • It terminates no TLS. No certificate, no minimum version, no cipher profile, no redirect to HTTPS. TLS stays end to end between the client and your backend — which is often the point: the gateway never holds the key material for that service.
  • The web application firewall does not apply to it. The firewall inspects requests, and there are none. A rule set cannot be attached to a TCP listener, and one attached to the gateway does not reach it.
  • The pool must be an HTTP pool, must not already receive traffic from a route, and must not use cookie session affinity — a cookie is an HTTP header the gateway has no way to set at layer 4.

Everything else about the policy edge applies unchanged. Source CIDR rules, country blocking, rate limits, connection caps and health checks all work the same way they do on an HTTPS listener:

bash
fm appgw listener create --gateway <gw-id> --name smtp \
  --protocol tcp --port 25 --backend-pool <pool-id> \
  --allowed-cidr 203.0.113.0/24 \
  --rate-limit-rps 20 --rate-limit-burst 40 \
  --max-connections 2000

That is the reason to reach for an Application Gateway rather than a Load Balancer for a non-HTTP service: you still get one address, one place the rules live, and one thing to audit.

On a TCP listener the rate limits count CONNECTIONS, not requests

On HTTP and HTTPS, --rate-limit-rps and --rate-limit-burst count requests. At layer 4 there is no request to count, so they count connections.

Where one connection carries a whole session — SMTP, IMAP, MQTT — the same number is a far tighter limit than it would be for a web listener. A figure copied across from an HTTPS listener will throttle a mail server by orders of magnitude. Set it against the connections you actually expect.

Port ranges ​

A TCP listener can bind a range of ports instead of one:

bash
fm appgw listener create --gateway <gw-id> --name ftp-data \
  --protocol tcp --port 30000 --port-range-end 30099 \
  --backend-pool <pool-id>

The range is inclusive, must run upwards from --port, and may span at most 512 ports. Each connection is forwarded to the same port on your backend that the client connected to — 30000-30099 reaches 30000-30099 on your servers. That is what makes a passive-FTP data range, or a block of per-service ports, work at all.

Two things follow from it:

  • A pool behind a ranged listener must give its health check a port of its own. Its backends are dialled on whatever port each client arrived on, so a probe has nothing to dial otherwise. See Health checks.
  • Two listeners on one gateway may not overlap in port space. A second listener claiming any port an existing one binds is refused, whether the clash is between two single ports or two ranges.

A few ports are the appliance's own and are refused for every protocol. If a range is refused for that reason the message names the port — move the range, or split it in two around it.

Deleting a listener closes its port immediately

Creating a listener opens its port on the gateway's public ingress, and deleting one closes it — both without waiting for an apply. So replacing a live TCP service by deleting the listener and creating a new one drops traffic for the gap between the two calls. Create the replacement first where you can, and to change a setting on the pool behind it, update the pool rather than rebuilding either.

Protecting a listener ​

These apply to every request on the listener, before routing:

bash
fm appgw listener create --gateway <gw-id> --name https \
  --protocol https --port 443 --certificate <certificate-id> \
  --allowed-cidr 203.0.113.0/24 --allowed-cidr 198.51.100.0/24 \
  --geo-block-mode allow --geo-country SE --geo-country NO \
  --rate-limit-rps 50 --rate-limit-burst 100 \
  --max-connections 4000

--max-connections is bounded by your size: a listener asking for more than the size is rated to hold raises the appliance's own limit to match, which is a way to exhaust it rather than a way to get more capacity. Check the rating with fm appgw flavors.

--allowed-cidr and --denied-cidr are repeatable. --geo-block-mode is off, allow (only these countries) or deny (everyone except these), with ISO country codes.

Backend pools and backends ​

A pool is a group of backends the gateway forwards to, plus how it picks between them and how it talks to them.

bash
fm appgw pool create --gateway <gw-id> --name app-pool \
  --protocol http --algorithm round_robin \
  --timeout-connect-ms 2000 --timeout-response-ms 30000

--algorithm is round_robin, least_connections or source_ip. For sticky sessions use --session-affinity cookie --session-cookie <name>, or --session-affinity source_ip.

To re-encrypt on the way to your application, make the pool https and say how the backend's certificate should be checked:

bash
fm appgw pool create --gateway <gw-id> --name secure-pool \
  --protocol https --tls-verify-backend \
  --tls-ca-certificate ./internal-ca.pem --tls-server-name app.internal

PROXY protocol — telling your backends who is calling ​

A backend behind a TCP listener sees the gateway's address as the client, not the real one. For a mail server that is close to fatal: spam scoring, rate limiting, greylisting and abuse logs are all computed against a single address, ours. There is no X-Forwarded-For to fall back on, because that is an HTTP header and a TCP listener carries no HTTP.

PROXY protocol puts the real client address in front of every connection:

bash
fm appgw pool create --gateway <gw-id> --name mail-pool \
  --protocol http --proxy-protocol

Configure the backend to read the header FIRST

Turning this on changes the bytes your backend receives. A server that is not expecting the header reads it as the first bytes of your protocol, and every connection fails.

The order matters, in both directions:

  1. Configure the backend to accept the PROXY protocol from the gateway — in Postfix that is smtpd_upstream_proxy_protocol, in nginx proxy_protocol on the listen line, in Dovecot the setting naming which upstreams may send it.
  2. Turn it on here.
  3. Apply the gateway configuration.

Turning it off again is the same change in reverse: take the setting off the gateway and apply, then stop the backend expecting it.

It is off by default, and stays off unless you ask for it.

Changing a pool without rebuilding it ​

Everything on a pool except its name can be changed in place:

bash
fm appgw pool update <pool-id> --gateway <gw-id> --proxy-protocol
fm appgw pool update <pool-id> --gateway <gw-id> \
  --algorithm least_connections --timeout-response-ms 60000

A setting you do not name is left alone, and the change takes effect at the next apply like every other piece of gateway configuration.

This is how PROXY protocol gets turned on for a pool that already exists. Without it the only way would be to delete the pool — which first means deleting the TCP listener holding it, closing that public port the instant you do — and build both again: an outage to change one setting whose whole purpose is to be changed once the backend has learned to read the header.

Two changes are refused while a TCP listener forwards to the pool, for the same reasons they are refused when creating that listener: cookie session affinity is an HTTP cookie the gateway cannot set at layer 4, and https as the pool protocol would re-encrypt bytes that are already whatever the client sent.

Backends are named by IP address inside the gateway's VPC, with a port:

bash
fm appgw backend add --gateway <gw-id> --pool <pool-id> \
  --address 10.0.1.10 --port 8080 --weight 1

An address outside the gateway's own VPC is refused. That is deliberate: without it the gateway would forward anywhere you named.

Health checks ​

A pool without a health check sends traffic to backends that are down.

bash
fm appgw health-check set --gateway <gw-id> --pool <pool-id> \
  --protocol http --path /healthz --expected-status 200-299 \
  --interval 10 --timeout 5 --healthy-threshold 2 --unhealthy-threshold 3

--timeout must be shorter than --interval.

--protocol tcp opens a connection to the port and closes it again. It sends no request and compares no status, so --path and --expected-status do not apply and are refused on it. Use it for anything that does not answer HTTP.

health-check set REPLACES the check — it does not merge into it

The command states the whole check. A setting you leave out does not stay as it was: it returns to the platform default. --port goes back to probing each backend's own port, and --proxy-protocol goes back to off.

So retuning one number means re-stating the rest. A check that probes 8080 with the PROXY protocol on, re-set with only --protocol, --path and --interval, silently loses both — and a probe that stops sending the header to a port that requires it fails every time, marking every backend in the pool unhealthy while the backends are fine. See PROXY protocol on the probe.

Read the check with fm appgw health-check get first and pass back what it had. To have no check at all, use health-check delete rather than an empty set.

Probing a different port ​

By default the probe dials each backend's own port. --port sends it somewhere else:

bash
fm appgw health-check set --gateway <gw-id> --pool <pool-id> \
  --protocol http --path /healthz --port 8080

Two cases want this. The first is a service whose health endpoint lives elsewhere — a mail server answering SMTP on 25 with a small HTTP health endpoint on 8080. Without it the only available probe is "open a connection to 25", which a loaded mail server will accept and then defer, so the backend reads healthy while it is refusing every message.

The second is not optional: a pool behind a ranged TCP listener must set --port. Its backends are dialled on whatever port each client arrived on, so there is no fixed port for a probe to reach, and a check without one is refused.

PROXY protocol on the probe ​

A pool that sends the PROXY protocol can have its health check send it on the probe connection too:

bash
fm appgw health-check set --gateway <gw-id> --pool <pool-id> \
  --protocol http --path /healthz --port 8080 --proxy-protocol

Whether you want that depends on which port the probe dials, and only you know the answer. A probe port exists precisely to reach a different service from the one carrying traffic — a mail server answering SMTP on 25 with a small health endpoint on 8080 — and a health endpoint like that usually has no reason to speak the PROXY protocol. The ordinary case does not want the header.

Send it to a port that is not expecting it and that port reads the header as the first bytes of your protocol. The probe fails, and a failing probe marks every backend in the pool unhealthy while the backends themselves are fine — the pool returns 503 for servers that are serving perfectly. Like the rest of the gateway's configuration this happens at the next apply, not when you set it.

Off does NOT mean "the probe sends no header"

A probe with no --port of its own dials each backend's own address and port, so it inherits the pool's connection settings — the PROXY protocol among them. Such a probe already carries the header whenever the pool's own PROXY protocol is on, with this setting left off.

Naming a probe --port opts the probe out of those settings, and takes the header with it. This setting is how you put it back.

fm appgw health-check get reports the two together for that reason: with no probe port, the row PROXY protocol on probe reads no (the probe follows the pool's setting) rather than a bare no.

The pool's own PROXY protocol has to be on first. Turning it on for the probe of a pool that sends no header is refused — there would be no header to send. The reverse is refused as well: turning the pool's off is refused while the probe still sends it. The order is pool on, then probe on, and probe off, then pool off.

Turning it on while the probe has no port of its own is accepted and stored, and changes nothing — that probe is inheriting the pool's setting already. It begins to mean something the moment the probe names a port.

In the portal it is a switch in the pool's health-check panel. In Terraform it is proxy_protocol on frostmoln_appgw_health_check, where the order above has one further consequence: a single terraform apply that turns both this and the pool's off fails on the pool, because Terraform updates the pool before the check that references it. Turn the probe's off in one apply, and the pool's in a later one.

Removing a health check ​

bash
fm appgw health-check delete --gateway <gw-id> --pool <pool-id>

From the next apply the gateway stops probing this pool and treats every enabled backend as available.

That is the right answer when the backends decide their own availability — a supervised service, or one sitting behind its own load balancer — and a probe from the gateway would only add a second opinion. For everything else, keep a check: without one, a backend that has stopped answering still receives its share of traffic.

What the gateway has observed ​

fm appgw backend list and the portal both show a status per backend, with the time it was last observed.

Backend health is not reported yet

The reporting that fills this in is still being built, so every backend currently reads not observed, whatever its real state. That is a statement about our reporting, not about your servers — an unobserved backend is neither healthy nor unhealthy as far as the platform is concerned, and nothing is being withheld from traffic because of it. This page will say so when it changes.

Authorize each backend — this step is not optional ​

Adding a backend tells the gateway where to forward. It does not give it permission to get there: your backends' security groups still decide that, and the gateway is not in them.

Authorize it explicitly:

bash
fm appgw backend authorize --gateway <gw-id> --pool <pool-id> \
  --backend <backend-id> --security-group <security-group-id>

This adds one ingress rule to your security group allowing the gateway in on that port. Nothing opens it for you — the platform will not quietly edit your security groups.

Two consequences worth knowing:

  • Skip it and your first gateway returns 503, because the connection to the backend is refused.
  • The rule is shared. It is written per security group, protocol and port, so every backend behind that group on that port is reachable through it — and revoking it closes the path for all of them, not just the one you named.
bash
fm appgw authorization list --gateway <gw-id>
fm appgw authorization revoke --gateway <gw-id> <authorization-id>

Routes — which service answers ​

A route matches a hostname and path on a listener and forwards to a pool.

bash
fm appgw route create --gateway <gw-id> --listener <listener-id> \
  --name api --host api.example.com --path /v1 --match-type prefix \
  --pool <pool-id>

--match-type is prefix, exact or regex. --host accepts a wildcard (*.example.com).

Routes are evaluated by priority, lowest first — an explicit number, never guessed from how specific the path looks. A new route goes last unless you say otherwise. Reorder without recreating anything:

bash
fm appgw route reorder --gateway <gw-id> --listener <listener-id> \
  --route <route-id>=10 --route <other-route-id>=20

Priority being explicit is what stops adding one route from silently changing where existing traffic goes.

A route can also rewrite the request before forwarding:

bash
fm appgw route create --gateway <gw-id> --listener <listener-id> \
  --name legacy --host example.com --path /old --match-type prefix \
  --pool <pool-id> --rewrite-path-prefix /new \
  --set-header X-Tenant=acme --remove-header X-Debug \
  --set-response-header Strict-Transport-Security="max-age=31536000"

Apply — nothing is live until you do ​

Listeners, routes, pools, backends and certificates are authored. They are saved as soon as you write them, and the gateway keeps serving what it already had until you apply:

bash
fm appgw apply <gateway-id>

The gateway validates the new configuration, swaps to it, reloads and checks itself before reporting back. If any of that fails it restores what it was serving and tells you why — a bad configuration takes your site down for no one.

fm appgw get <gateway-id> shows whether what you authored is what is running. The portal shows the same thing as a banner, and both name the appliance's own reason when an apply is refused.

Apply needs the gateway to be running, and only one apply runs at a time.

Web application firewall ​

Every Application Gateway includes a web application firewall, at no extra charge — there is no separate product to buy and no add-on to enable. Rules are authored per gateway, on top of a maintained baseline that Frostmoln keeps current.

Not yet available

Rule authoring is not open to customers yet. This page will document it when it is; nothing in your gateway's configuration will change in the meantime.

Deleting a gateway ​

bash
fm appgw delete <gateway-id>

Traffic stops immediately. Whether the public address survives depends on how the gateway got it: an address the platform allocated is released, and an address you brought is kept and returns to your Public IPs.

Billing ​

An Application Gateway is billed per hour on its size, like any other managed instance, and the web application firewall is included in that price. If the gateway's public address was allocated by the platform it is billed as a Public IP, separately and only once; an address you already hold is already on your bill and is not billed twice.

Availability ​

A standard Application Gateway is a single managed appliance. If its host fails it is rebuilt, which is not instant. Plan for that the way you would for any single instance — and if a short interruption is unacceptable for the traffic you are fronting, say so before you put production behind one.