HTTP Redirects Reference
Nexus GSLB includes a built-in HTTP redirect service comparable to NS1's Redirect Manager. It listens on a configurable port, matches incoming requests by Host header against a stored rule table, and responds with HTTP 301, 302, 307, or 308. Rules are managed via the REST API and take effect immediately on all cluster nodes without a restart.
License: HTTP redirects require the paid tier or above. The redirect listener is disabled at startup on free and unlicensed installations. Contact licensing@gslb.nexus to upgrade.
Overview
| Property | Value |
|---|---|
| Listener | Configurable port (typically 80) |
| Match key | HTTP Host header (case-insensitive, port and trailing dot stripped) |
| Redirect types | 301 Permanent, 302 Temporary, 307 Temporary (method-preserving), 308 Permanent (method-preserving) |
| Path handling | Optional path + query string preservation |
| Rule activation | Immediate — live handler updated on API create/delete |
| Cluster sync | Rules replicated via NATS state sync to all nodes |
Configuration
redirect:
enabled: true
listenAddr: "0.0.0.0"
port: 80| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Start the redirect listener. Must be true to serve any redirects. |
listenAddr |
string | 0.0.0.0 |
Bind address for the redirect HTTP listener. |
port |
int | — | Port to listen on. Required when enabled: true. |
The redirect listener is a plain HTTP server (no TLS). To serve HTTPS redirects, run the listener behind Caddy or nginx, which terminates TLS and proxies to the redirect port.
Proxy integrations
When Nexus runs behind a reverse proxy (Caddy, Traefik, nginx on bare metal; or any Gateway API-conformant controller in Kubernetes), the built-in redirect listener may not be reachable by external clients. Two integrations allow Nexus to push redirect rules directly into the proxy when rules are created or deleted.
Kubernetes Gateway API
Nexus creates and manages HTTPRoute resources in a Kubernetes cluster. Any conformant Gateway controller — Envoy Gateway, Istio, Traefik v3, Nginx Gateway Fabric, Cilium, Kong — will pick up the route and serve the redirect.
Each redirect rule produces one HTTPRoute named nexus-redirect-<id> in the configured namespace. The route attaches to the specified Gateway via parentRefs and uses a RequestRedirect filter to return the correct status code and destination.
redirect:
enabled: true
listenAddr: "0.0.0.0"
port: 80
integrations:
gatewayApi:
enabled: true
kubeconfig: "" # path to kubeconfig file; empty = in-cluster credentials
gatewayName: "my-gateway"
gatewayNamespace: "default"
httprouteNamespace: "default"| Field | Type | Required | Description |
|---|---|---|---|
enabled |
bool | yes | Enable the integration. |
kubeconfig |
string | no | Path to a kubeconfig file. Leave empty when Nexus runs inside the cluster (uses in-cluster credentials). |
gatewayName |
string | yes | Name of the Gateway resource the HTTPRoute will attach to. |
gatewayNamespace |
string | yes | Namespace of the Gateway resource. |
httprouteNamespace |
string | yes | Namespace in which HTTPRoute objects are created. |
Required RBAC — the service account or kubeconfig user must have create, get, update, and delete on httproutes.gateway.networking.k8s.io in httprouteNamespace. Example manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: nexus-gslb-redirect
namespace: default # must match httprouteNamespace
rules:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["get", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: nexus-gslb-redirect
namespace: default
subjects:
- kind: ServiceAccount
name: nexus-gslb # service account Nexus runs as
namespace: nexus-system
roleRef:
kind: Role
name: nexus-gslb-redirect
apiGroup: rbac.authorization.k8s.iopreservePath behaviour — when preservePath: true, no path modifier is set on the RequestRedirect filter so the upstream controller preserves the original path. When preservePath: false and the target URL contains a non-root path, a ReplaceFullPath modifier is set.
Caddy
Nexus manages redirect routes in a running Caddy instance via its admin API. Each rule is stored with a stable @id (nexus-redirect-<id>) so it can be updated or removed without affecting other routes Caddy may manage.
redirect:
enabled: true
listenAddr: "0.0.0.0"
port: 80
integrations:
caddy:
enabled: true
adminUrl: "http://localhost:2019" # default
serverName: "srv0" # default| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | — | Enable the integration. |
adminUrl |
string | http://localhost:2019 |
URL of the Caddy admin API. Must be reachable from the Nexus process. |
serverName |
string | srv0 |
Caddy server name in the config tree. Run curl localhost:2019/config/apps/http/servers to list available servers. |
preservePath behaviour — when preservePath: true, the Location header uses Caddy's {http.request.uri} placeholder to append the original path and query. When preservePath: false, the target URL is used verbatim.
Note — the Caddy admin API must be accessible from the Nexus process. By default it binds to localhost:2019 and is not authenticated; consider enabling admin.enforce_origin and admin.origins in your Caddyfile if the admin API is exposed beyond localhost.
Nginx
Nexus generates a single nginx server {} block config file containing all active redirect rules, writes it to disk, and signals nginx to reload. This covers bare-metal and VM deployments where nginx is the TLS-terminating front-end.
redirect:
enabled: true
listenAddr: "0.0.0.0"
port: 80
integrations:
nginx:
enabled: true
configDir: "/etc/nginx/conf.d" # must be writable by the gslbd process
configFile: "nexus-redirects.conf" # default; omit to use this value
reloadCommand: "nginx -s reload" # default; override for systemd or sudo| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | — | Enable the integration. |
configDir |
string | — | Required. Directory where the config file is written (e.g. /etc/nginx/conf.d). Must be writable by the gslbd process. |
configFile |
string | nexus-redirects.conf |
Filename within configDir. The file is fully regenerated on every rule change. |
reloadCommand |
string | nginx -s reload |
Shell command to reload nginx. Split on whitespace; not passed through a shell. Use systemctl reload nginx or sudo nginx -s reload as needed. |
Generated config format:
# Generated by Nexus GSLB — do not edit manually
# Updated: 2025-01-01T00:00:00Z
server {
listen 80;
server_name old.example.com;
return 301 https://new.example.com$request_uri;
}
When preservePath: false, $request_uri is omitted and the target URL is used verbatim. When the rule list is empty, a comment-only file is written and nginx is still reloaded (to remove previously written rules).
Permissions — gslbd must be able to write configDir/configFile and execute the reload command. A common setup:
# Allow gslbd to write the config file
chown gslbd:nginx /etc/nginx/conf.d/nexus-redirects.conf
# Allow gslbd to reload nginx without a password (sudoers)
echo "gslbd ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload" > /etc/sudoers.d/nexus-nginx
# Then set reloadCommand: "sudo nginx -s reload"Cloudflare
Nexus manages Redirect Rules in one or more Cloudflare zones via the Ruleset Engine API. Each Nexus redirect rule is stored as a single entry in the zone's http_request_redirect ruleset, identified by the description nexus-redirect-<id>.
redirect:
enabled: true
listenAddr: "0.0.0.0"
port: 80
integrations:
cloudflare:
enabled: true
apiToken: "your-cloudflare-api-token"
zones:
- zoneId: "abc123def456"
domains:
- "example.com"
- "example.net"
- zoneId: "xyz789"
domains: [] # catch-all: matches any source FQDN not claimed above| Field | Type | Required | Description |
|---|---|---|---|
enabled |
bool | yes | Enable the integration. |
apiToken |
string | yes | Cloudflare API token. Must have Zone → Rules → Edit permission on all configured zones. |
zones[].zoneId |
string | yes | Cloudflare zone ID (found in the zone's Overview page). |
zones[].domains |
[]string | no | Source FQDNs (or domain suffixes) that belong to this zone. A redirect for old.example.com matches a zone with domain example.com. Leave empty for a catch-all zone used when no other zone matches. |
Zone matching — Nexus checks each zone's domains list in order and uses the first match. A zone with no domains configured is used as a fallback. If no zone matches a source FQDN, the integration logs a warning and skips the rule.
preservePath behaviour — when preservePath: false, the Cloudflare rule uses a static target URL. When preservePath: true, the rule uses a Cloudflare expression concat("<target>", http.request.uri) which preserves both path and query string.
Required API token permissions:
- Zone → Rules → Edit (for each zone)
- Zone → Zone → Read (for zone lookup, if using the API to discover zone IDs)
Create a scoped token at Cloudflare Dashboard → My Profile → API Tokens → Create Token.
Data model
RedirectRule
| Field | Type | Description |
|---|---|---|
id |
string | UUID assigned on creation. |
tenantId |
string | Tenant that owns this rule. |
sourceFqdn |
string | The source hostname (e.g. old.gslb.cc). Matched against the HTTP Host header. |
targetUrl |
string | The full URL to redirect to (e.g. https://new.gslb.cc). |
code |
int | HTTP redirect status code. 301 (permanent), 302 (temporary), 307 (temporary, method-preserving), or 308 (permanent, method-preserving). |
preservePath |
bool | When true, the request path and query string are appended to targetUrl. |
createdAt |
int64 | Unix timestamp of creation. |
API
List redirect rules
GET /api/v1/redirects
Returns all redirect rules for the current tenant.
Response 200 OK: array of RedirectRule objects.
[
{
"id": "a1b2c3...",
"tenantId": "default",
"sourceFqdn": "old.gslb.cc",
"targetUrl": "https://new.gslb.cc",
"code": 301,
"preservePath": true,
"createdAt": 1716100000
}
]Create a redirect rule
POST /api/v1/redirects
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
sourceFqdn |
string | yes | — | Source hostname, e.g. old.gslb.cc. Trailing dot optional. Must be a valid DNS hostname (letters, digits, -, _, dot-separated labels). |
targetUrl |
string | yes | — | Destination URL, e.g. https://new.gslb.cc. Must be an absolute http/https URL. Values containing control characters or the delimiters ; { } # " ' \ or whitespace are rejected. |
code |
int | no | 301 |
301, 302, 307, or 308. Any other value is silently normalised to 301. |
preservePath |
bool | no | true |
Append the incoming path and query string to targetUrl. |
Response 201 Created: the created RedirectRule object. 400 Bad Request if sourceFqdn or targetUrl fails validation.
Why the strict validation: both fields are rendered into generated proxy configuration (nginx
server_name/returndirectives, Caddy/Cloudflare/Gateway objects). Rejecting delimiter and control characters at this write boundary prevents a rule from injecting directives into the generated config. The nginx renderer independently skips any rule with unsafe characters as defense-in-depth.
The rule is loaded into the live redirect handler on all cluster nodes immediately; no restart is required.
Example — permanent redirect with path preservation:
curl -X POST https://nexus-api.example.com/api/v1/redirects \
-H "Authorization: Bearer $GSLB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceFqdn": "old.gslb.cc",
"targetUrl": "https://new.gslb.cc",
"code": 301,
"preservePath": true
}'Example — temporary redirect to a fixed URL (no path):
curl -X POST https://nexus-api.example.com/api/v1/redirects \
-H "Authorization: Bearer $GSLB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceFqdn": "maintenance.gslb.cc",
"targetUrl": "https://status.gslb.cc",
"code": 302,
"preservePath": false
}'Delete a redirect rule
DELETE /api/v1/redirects/{id}
Removes the rule from the database and from the live handler on all cluster nodes immediately.
Response 204 No Content.
Path and query preservation
When preservePath: true, the request URI (path + query string) is appended to the base targetUrl. The trailing slash on targetUrl is normalised to avoid double slashes.
| Request | targetUrl |
preservePath |
Redirect destination |
|---|---|---|---|
GET / |
https://new.gslb.cc |
true |
https://new.gslb.cc |
GET /docs/api |
https://new.gslb.cc |
true |
https://new.gslb.cc/docs/api |
GET /search?q=foo |
https://new.gslb.cc |
true |
https://new.gslb.cc/search?q=foo |
GET /anything |
https://new.gslb.cc |
false |
https://new.gslb.cc |
When preservePath: false, all requests to the source hostname redirect to targetUrl verbatim, regardless of the path.
Host matching
The incoming Host header is normalised before lookup:
- Converted to lower case
- Port stripped (e.g.
old.gslb.cc:8080→old.gslb.cc) - Trailing dot removed (e.g.
old.gslb.cc.→old.gslb.cc)
The sourceFqdn stored in the rule is normalised the same way at write time, so rules stored with or without a trailing dot match correctly.
If no rule matches the Host header, the redirect server responds 404 Not Found.
Permissions
| Permission | Grants |
|---|---|
redirect:read |
List redirect rules |
redirect:write |
Create and delete redirect rules |
Default role assignments:
| Role | Permissions |
|---|---|
tenant_admin |
redirect:read, redirect:write |
operator |
redirect:read, redirect:write |
viewer |
redirect:read |
Cluster state sync
Redirect rules are included in the cluster state snapshot and replicated via NATS to all nodes when the snapshot is published. Additionally, create and delete API operations call SetRedirectManager / Delete on the local node's in-memory handler directly, so the change takes effect on that node without waiting for the next sync cycle.
When a new node joins and receives its initial snapshot, all current redirect rules are loaded into its handler before it starts serving traffic.
DNS wiring
The redirect service handles the HTTP layer only. To make old.gslb.cc resolve to the Nexus nodes, you must create a GSLB service or DNS record pointing that hostname at the nodes running the redirect listener.
Typical setup:
- Create a pool with the Nexus node IPs as members
- Create a service:
old.gslb.cc→ that pool - Create a redirect rule:
old.gslb.cc→https://new.gslb.cc - Enable the redirect listener on port 80 in the config
HTTP traffic for old.gslb.cc hits the Nexus node, the redirect listener serves the 301, and the browser follows it to https://new.gslb.cc.
Troubleshooting
Redirect listener not starting
Check systemctl status gslbd and look for "redirect listener starting" in the logs. Common causes:
redirect.enabledisfalse(default)- Port 80 is already bound by Caddy, nginx, or another process — use a different port and proxy to it
Rule not taking effect after creation
The rule is applied to the local node's handler immediately on API create. If requests hit a different node and that node hasn't received the state sync yet, there may be a brief window where the rule is absent. Verify with:
curl -v http://<each-node-ip>/ -H "Host: old.gslb.cc"404 Not Found from redirect server
No rule matches the Host header. Verify the sourceFqdn in the stored rule matches the Host header exactly (after normalisation). Check with:
curl -s https://nexus-api.example.com/api/v1/redirects \
-H "Authorization: Bearer $GSLB_API_KEY" | jq '.[].sourceFqdn'Code references
internal/redirect/redirect.go:Handler,Rule,ServeHTTP,buildTargetinternal/redirect/integration.go:Integrationinterfaceinternal/redirect/integration_gateway.go:GatewayAPIIntegrationinternal/redirect/integration_caddy.go:CaddyIntegrationinternal/redirect/integration_nginx.go:NginxIntegration,renderNginxConfiginternal/redirect/integration_cloudflare.go:CloudflareIntegrationinternal/storage (rqlite.go + storage_*.go):RedirectRule,CreateRedirectRule,ListRedirectRules,ListAllRedirectRulesinternal/storage/tenant.go: tenant-scoped wrapper methodsinternal/api/handlers_redirect.go:listRedirects,createRedirect,deleteRedirectinternal/api/rbac.go:PermRedirectRead,PermRedirectWritecmd/gslbd/setup_dns.go: startup wiring — integration init, DB hydration,SetRedirectManager