Skip to content
Merged
111 changes: 111 additions & 0 deletions examples/ingress-resources/rewrite-target/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Support for Rewrite Target

The `nginx.org/rewrite-target` annotation enables URL path rewriting by specifying a target path that requests should be rewritten to. This annotation works with regular expression capture groups from the Ingress path to create dynamic rewrites.

The annotation is mutually exclusive with `nginx.org/rewrites`. If both are present, `nginx.org/rewrites` takes precedence.

## Running the Example

## 1. Deploy the Ingress Controller

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) instructions to deploy the Ingress Controller.

2. Save the public IP address of the Ingress Controller into a shell variable:
```console
IC_IP=XXX.YYY.ZZZ.III
```

3. Save the HTTP port of the Ingress Controller into a shell variable:
```console
IC_HTTP_PORT=<port number>
```

## 2. Deploy the Cafe Application

Create the coffee and tea deployments and services:

```console
kubectl create -f cafe.yaml
```

## 3. Configure Rewrite Examples

### Example 1: Simple Static Rewrite

Create an Ingress resource with basic rewrite functionality:

```console
kubectl create -f simple-rewrite.yaml
```

This configures rewriting from `/coffee` to `/beverages/coffee`.

### Example 2: Dynamic Rewrite with Regex

Create an Ingress resource with regular expression-based rewriting:

```console
kubectl create -f regex-rewrite.yaml
```

This configures dynamic rewriting using capture groups from `/menu/([^/]+)/([^/]+)` to `/beverages/$1/$2`.

## 4. Test the Application

### Test Simple Rewrite

Access the coffee service through the rewritten path:

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP http://cafe.example.com:$IC_HTTP_PORT/coffee --insecure
```

```text
Server address: 10.16.0.16:8080
Server name: coffee-676c9f8944-n2bmb
Date: 07/Nov/2025:11:23:09 +0000
URI: /beverages/coffee
Request ID: c224b3e06d79b66f8f33e86cef046c32
```

The request to `/coffee` is rewritten to `/beverages/coffee`.

### Test Regex Rewrite

Access the service using the menu path with dynamic rewriting:

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP http://cafe.example.com:$IC_HTTP_PORT/menu/coffee/espresso --insecure
```

```text
Server address: 10.16.1.29:8080
Server name: coffee-676c9f8944-vj45p
Date: 07/Nov/2025:11:26:05 +0000
URI: /beverages/coffee/espresso
Request ID: 88334a8b0eeaee2ffe4fdb4c7768641b
```

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP http://cafe.example.com:$IC_HTTP_PORT/menu/tea/green --insecure
```

```text
Server address: 10.16.0.16:8080
Server name: coffee-676c9f8944-n2bmb
Date: 07/Nov/2025:11:26:33 +0000
URI: /beverages/tea/green
Request ID: 2ba8f9055aecc059b32f797f1ce2aca5
```

The requests to `/menu/coffee/espresso` and `/menu/tea/green` are rewritten to `/beverages/coffee/espresso` and `/beverages/tea/green` using the captured groups.

## Validations

1. Mutual Exclusivity: The `nginx.org/rewrite-target` annotation is mutually exclusive with `nginx.org/rewrites`. If both annotations are present, `nginx.org/rewrites` takes precedence and a warning will be generated.

2. Security Validation: The annotation includes built-in security validation to prevent:
- Absolute URLs (`http://` or `https://`)
- Protocol-relative URLs (`//`)
- Path traversal patterns (`../` or `..\\`)
- Paths not starting with `/`
65 changes: 65 additions & 0 deletions examples/ingress-resources/rewrite-target/cafe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 2
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 3
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
20 changes: 20 additions & 0 deletions examples/ingress-resources/rewrite-target/regex-rewrite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress
annotations:
nginx.org/path-regex: "case_sensitive"
nginx.org/rewrite-target: "/beverages/$1/$2"
spec:
ingressClassName: nginx
rules:
- host: cafe.example.com
http:
paths:
- path: /menu/([^/]+)/([^/]+)
pathType: ImplementationSpecific
backend:
service:
name: coffee-svc
port:
number: 80
19 changes: 19 additions & 0 deletions examples/ingress-resources/rewrite-target/simple-rewrite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress
annotations:
nginx.org/rewrite-target: "/beverages/coffee"
spec:
ingressClassName: nginx
rules:
- host: cafe.example.com
http:
paths:
- path: /coffee
pathType: Prefix
backend:
service:
name: coffee-svc
port:
number: 80
23 changes: 23 additions & 0 deletions internal/configs/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const BasicAuthSecretAnnotation = "nginx.org/basic-auth-secret" // #nosec G101
// PathRegexAnnotation is the annotation where the regex location (path) modifier is specified.
const PathRegexAnnotation = "nginx.org/path-regex"

// RewriteTargetAnnotation is the annotation where the regex-based rewrite target is specified.
const RewriteTargetAnnotation = "nginx.org/rewrite-target"

// SSLCiphersAnnotation is the annotation where SSL ciphers are specified.
const SSLCiphersAnnotation = "nginx.org/ssl-ciphers"

Expand Down Expand Up @@ -590,6 +593,26 @@ func getRewrites(ctx context.Context, ingEx *IngressEx) map[string]string {
return nil
}

func getRewriteTarget(ctx context.Context, ingEx *IngressEx) (string, Warnings) {
l := nl.LoggerFromContext(ctx)
warnings := newWarnings()

// Check for mutual exclusivity
if _, hasRewrites := ingEx.Ingress.Annotations["nginx.org/rewrites"]; hasRewrites {
if _, hasRewriteTarget := ingEx.Ingress.Annotations[RewriteTargetAnnotation]; hasRewriteTarget {
warningMsg := "nginx.org/rewrites and nginx.org/rewrite-target annotations are mutually exclusive; nginx.org/rewrites will take precedence"
nl.Errorf(l, "Ingress %s/%s: %s", ingEx.Ingress.Namespace, ingEx.Ingress.Name, warningMsg)
warnings.AddWarning(ingEx.Ingress, warningMsg)
return "", warnings
}
}

if value, exists := ingEx.Ingress.Annotations[RewriteTargetAnnotation]; exists {
return value, warnings
}
return "", warnings
}

func getSSLServices(ingEx *IngressEx) map[string]bool {
if value, exists := ingEx.Ingress.Annotations["nginx.org/ssl-services"]; exists {
return ParseServiceList(value)
Expand Down
Loading