Serverless frontends

The nginx setup runs on a VPS: a host that can be seized, imaged, and subpoenaed. A serverless frontend moves the routing logic into managed compute with no persistent disk, no long-lived process, and no box to hand over. When the function returns, the execution environment evaporates. The TLS endpoint is a cloud provider hostname, which carries that provider’s reputation rather than a fresh VPS with none.

The cost is identity. Lambda and Cloud Run tie the account to a real billing identity, which puts them in the same category as the identity-bound providers to avoid for target-facing infrastructure. Where the engagement scope tolerates that linkage, or where the serverless layer sits on the management side only, the operational conveniences are real.

Lambda as a reverse proxy

AWS API Gateway with a Lambda behind it gives an HTTPS endpoint at a *.execute-api.<region>.amazonaws.com hostname, with a managed certificate and no server to patch. The function receives the API Gateway event and forwards it upstream.

import json, urllib.request, urllib.error

UPSTREAM = "https://frontend.<otherdomain>.com"

def handler(event, context):
    method  = event.get("httpMethod", "GET")
    path    = event.get("path", "/")
    headers = event.get("headers") or {}
    body    = event.get("body") or ""

    req = urllib.request.Request(
        UPSTREAM + path,
        data=body.encode() if body else None,
        headers={k: v for k, v in headers.items()
                 if k.lower() not in ("host", "x-forwarded-for")},
        method=method,
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return {
                "statusCode": resp.status,
                "headers": dict(resp.headers),
                "body": resp.read().decode(errors="replace"),
            }
    except urllib.error.HTTPError as e:
        return {"statusCode": e.code, "headers": {}, "body": ""}

Deploy under a custom domain mapped through API Gateway if the execute-api hostname is too recognisable for the engagement. The masquerading rules still apply: the domain the target sees determines the category verdict.

GCP Cloud Run

Cloud Run serves a container behind a managed HTTPS endpoint at *.run.app. A minimal Go container that reverse-proxies upstream fits in a few dozen megabytes and starts cold in under a second:

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
)

func main() {
    upstream, _ := url.Parse(os.Getenv("UPSTREAM"))
    proxy := httputil.NewSingleHostReverseProxy(upstream)
    proxy.Director = func(req *http.Request) {
        req.URL.Scheme = upstream.Scheme
        req.URL.Host   = upstream.Host
        req.Host       = upstream.Host
    }
    http.ListenAndServe(":"+os.Getenv("PORT"), proxy)
}

Set UPSTREAM to the actual backend or next-hop nginx. Cloud Run scales to zero between requests, so there is nothing running between beacon intervals.

Trade-offs

  • The provider logs every invocation with timestamp, source IP, and payload size. The execution environment evaporates; the log does not.

  • Cold starts add latency. A function that has not run recently takes a second or two to respond, which a C2 beacon profile with tight timeout values may read as a dead channel and rotate away from.

  • Custom domains on API Gateway and Cloud Run require DNS records, which ties back to the domain opsec question.

  • Serverless functions are trivially torn down and redeployed under a new URL, which recovers some of the rotation speed the nginx VPS loses when the host takes minutes to reprovision.