Redirecting extra domains to the main domain with a 301 from an ALB listener rule

One extra SNI certificate and a host-header rule on the HTTPS listener 301 alias domains to the canonical one, keeping path and query. With DNS and curl checks.

A site often has several aliases: an old domain, the www and non-www forms, a backup domain. They should all return a 301 permanent redirect to a single canonical domain and keep the path and query string. If you already have an HTTPS listener, one ACM certificate and one ALB listener rule are enough. The requests never reach WordPress and never wake the database. This article follows what this site actually does and covers certificate validation, SNI, how to write the rule, and verification with curl. In the examples, example.org and www.example.org are the names being redirected away, and www.example.com is the canonical domain.

Architecture

Visitor ─TLS─> ALB HTTPS listener (:443)
                ├─ extra certificate for example.org / www.example.org in the certificate list (SNI)
                └─ rule: Host ∈ {example.org, www.example.org}
                     └─> 301 to https://www.example.com/original-path?original-query

DNS for these names points straight at the ALB, not at CloudFront. The redirect is completed on the ALB and the request never reaches the origin, so this rule does not need the secret header that CloudFront adds to origin requests. This site keeps it in a separate CDK stack: the WordPress stack stays unchanged, and when the redirect is no longer needed you delete the whole stack. The stack does exactly three things: request a certificate, add it to the listener, and add a rule.

Certificate: DNS validation when DNS is not in Route 53

A certificate used by an ALB must be requested in the same Region as the ALB. CloudFront is different: it only accepts certificates from us-east-1. Request one certificate covering several names with CDK:

const [first, ...rest] = props.from;          // ["example.org", "www.example.org"]
const cert = new acm.Certificate(this, "Certificate", {
  domainName: first,
  subjectAlternativeNames: rest,
  validation: acm.CertificateValidation.fromDns(),
});

When DNS is not in Route 53, CDK cannot write the validation records for you. The deployment stops at the certificate and waits until you manually add the CNAME records that ACM provides at your DNS provider. The record names and values are in the stack events, in the ACM console, or in the output of aws acm describe-certificate. A few things to watch:

  • Each name has its own validation CNAME (a wildcard name and its base domain share one).
  • DNS providers treat the “record name” field differently: some expect the full name, others append the domain automatically. Get it wrong and you end up with a record like _xxx.example.org.example.org, and validation fails.
  • If ACM cannot validate within 72 hours, the certificate status changes to Validation timed out and you have to request a new certificate.
  • Keep the validation records permanently. As long as the certificate is in use and the records are in place, ACM renews it automatically.

Adding it to the listener as an extra SNI certificate

An HTTPS listener has exactly one default certificate and can also carry a certificate list. When a client sends a hostname via SNI, the ALB picks the matching certificate from the list; it falls back to the default certificate only when nothing matches or the client does not support SNI. Adding a certificate therefore does not affect other sites already on the listener:

new elbv2.CfnListenerCertificate(this, "ListenerCertificate", {
  listenerArn: props.listenerArn,
  certificates: [{ certificateArn: cert.certificateArn }],
});

Rule: host-header condition + redirect action

new elbv2.CfnListenerRule(this, "Redirect", {
  listenerArn: props.listenerArn,
  priority: 110,
  conditions: [{ field: "host-header", hostHeaderConfig: { values: ["example.org", "www.example.org"] } }],
  actions: [{
    type: "redirect",
    redirectConfig: {
      protocol: "HTTPS",
      host: "www.example.com",
      port: "443",
      path: "/#{path}",
      query: "#{query}",
      statusCode: "HTTP_301",
    },
  }],
});

Key points:

  • #{path} and #{query} are ALB reserved keywords for the path and query string of the original request. The path must start with /, hence /#{path}; the query is written without the question mark.
  • At least one of protocol, host, port or path must change, otherwise the redirect loops. Here the host changes.
  • Priorities must be unique within a listener. The site’s own forwarding rule is 100; this one is 110. The Host sets of the two rules do not overlap, so their order does not affect the result.
  • ALB does not allow redirects from HTTPS to HTTP.

Verifying with curl

$ curl -sI "https://example.org/some/path?a=1&b=2" | grep -iE '^(HTTP|location|server)'
HTTP/2 301
server: awselb/2.0
location: https://www.example.com:443/some/path?a=1&b=2

The response comes from the ALB (server: awselb/2.0), and both the path and the query string are preserved. Tests on this site also confirmed the following:

  • Location includes :443, because the rule sets the port explicitly. 443 is the default port for https, so browsers and curl treat it as the same address when following the redirect. The result is the same; the URL just has an extra segment.
  • Without a query string, Location does not end with a stray ?, for example https://www.example.com:443/.
  • Percent-encoded paths and parameters are kept as-is, for example /a%20b/c?x=%E4%B8%AD.
  • Requests over http are redirected twice: first by the existing HTTP→HTTPS redirect on the ALB’s port 80 listener to https://example.org:443/…, then by this rule to the canonical domain. curl -sIL -o /dev/null -w '%{num_redirects} %{url_effective}\n' http://example.org/x shows the number of redirects and the final URL.

Why do it on the ALB

  • The application is left alone: redirecting inside WordPress means every request reaches the application container, and with a plugin it also goes through PHP and the database. This site’s Aurora pauses when idle, and a single crawler working through the old domain could wake it up again and again.
  • The CDN is untouched: these names do not need to be added as alternate domain names on the CloudFront distribution, and they do not need a certificate in us-east-1.
  • Clear boundaries: the separate stack only adds resources and does not modify existing ones. The listener’s default certificate, default action and other rules all stay as they were.

Limitations

  • Under the DNS specification, an apex domain (such as example.org) cannot have a CNAME. If DNS is not in Route 53, make sure your provider offers something like CNAME flattening before you point the apex at the ALB’s DNS name.
  • Browsers cache 301s for a long time, so a wrong target is hard to take back. Test with HTTP_302 first and switch to 301 once everything is confirmed.
  • These requests do not go through CloudFront, so they get none of CloudFront’s caching or protection; the ALB takes them directly.

Links