Warning
This project is under active development and evolving quickly. Some features may be missing, incomplete, or subject to change. Use in production at your own discretion.
Dynamic HTTP routing for Kubernetes with Envoy and Istio. Define your routing rules as Kubernetes resources and let CustomRouter handle the rest.
CustomRouter consists of two components that work together:
- Operator: Watches
CustomHTTPRouteresources and compiles them into optimized routing tables - External Processor: An Envoy ext_proc that receives requests from your gateway and routes them based on the compiled rules
flowchart LR
subgraph Kubernetes
CHR[CustomHTTPRoute]
EPA[ExternalProcessorAttachment]
OP[Operator]
CM[ConfigMap]
EF[EnvoyFilter]
end
subgraph "Istio Gateway"
GW[Gateway Pod]
EP[External Processor]
end
subgraph Backend
SVC1[Service A]
SVC2[Service B]
SVC3[Service C]
end
CHR -->|watches| OP
EPA -->|watches| OP
OP -->|generates| CM
OP -->|generates| EF
CM -->|loads routes| EP
EF -->|configures| GW
GW -->|ext_proc call| EP
EP -->|routing decision| GW
GW --> SVC1
GW --> SVC2
GW --> SVC3
- You create
CustomHTTPRouteresources defining your routing rules - The operator compiles these rules into optimized ConfigMaps
- You create an
ExternalProcessorAttachmentto connect the external processor to your gateway - The operator generates EnvoyFilters that configure Istio to use the external processor
- Incoming requests hit the gateway, which calls the external processor
- The external processor matches the request against the routing rules and tells Envoy where to send it
- Kubernetes v1.26+
- Istio v1.18+ (for gateway integration)
- Go v1.24+ (for development)
helm install customrouter oci://ghcr.io/freepik-company/customrouter/helm-chart/customrouter \
--namespace customrouter \
--create-namespaceapiVersion: customrouter.freepik.com/v1alpha1
kind: CustomHTTPRoute
metadata:
name: my-routes
spec:
# Target identifies which external processor handles these routes
targetRef:
name: default
# Hostnames this route applies to
hostnames:
- www.example.com
- example.com
# Optional path prefixes (e.g., for i18n)
pathPrefixes:
values: [es, fr, de]
policy: Optional # Optional | Required | Disabled
# Control which match types get prefix expansion (default: all)
expandMatchTypes: [PathPrefix, Exact] # Only expand these types
rules:
# Simple prefix match (default)
- matches:
- path: /api
backendRefs:
- name: api-service
namespace: backend
port: 8080
# Exact match with high priority
- matches:
- path: /health
type: Exact
priority: 2000
backendRefs:
- name: health-service
namespace: infra
port: 8080
# Regex match
- matches:
- path: ^/users/[0-9]+/profile$
type: Regex
backendRefs:
- name: users-service
namespace: backend
port: 8080
# Default fallback (low priority)
- matches:
- path: /
priority: 100
backendRefs:
- name: default-service
namespace: web
port: 80apiVersion: customrouter.freepik.com/v1alpha1
kind: ExternalProcessorAttachment
metadata:
name: production-gateway
namespace: istio-system
spec:
# Select which gateway pods to attach to
gatewayRef:
selector:
istio: gateway-production
# Reference to the external processor service
externalProcessorRef:
service:
name: customrouter-extproc
namespace: customrouter
port: 9001
# Optional: Generate catch-all routes for hostnames without HTTPRoute
# This allows CustomHTTPRoute to handle requests without requiring
# a base HTTPRoute to be configured separately
catchAllRoute:
hostnames:
- example.com
- www.example.com
backendRef:
name: default-backend
namespace: web
port: 80The chart supports deploying the operator and multiple external processors:
# values.yaml
operator:
enabled: true
replicaCount: 1
args:
- --leader-elect
- --health-probe-bind-address=:8081
externalProcessors:
# Default processor
default:
enabled: true
replicaCount: 2
args:
- --addr=:9001
- --target-name=default
- --routes-configmap-namespace=default
- --access-log=true
# Additional processor for different routes
internal:
enabled: true
replicaCount: 1
args:
- --addr=:9001
- --target-name=internal
- --routes-configmap-namespace=default
- --access-log=false
# Deploy additional resources
extraObjects:
- apiVersion: customrouter.freepik.com/v1alpha1
kind: CustomHTTPRoute
metadata:
name: my-routes
spec:
targetRef:
name: default
# ...See chart/values.yaml for all available options.
- Go v1.24+
- Docker
- kubectl
- A Kubernetes cluster (kind, minikube, etc.)
# Build all binaries
make build-all
# Build only the operator
make build
# Build only the external processor
make build-extproc# Install CRDs
make install
# Run the operator locally
make run
# Run the external processor locally
make run-extproc# Run unit tests
make test
# Run e2e tests (requires a cluster)
make test-e2e# Build all images
make docker-build-all
# Build and push all images
make docker-build-all docker-push-all# Generate CRDs, RBAC, and DeepCopy methods
make generate manifestsRun make help to see all available targets:
Usage:
make <target>
General:
help Display this help
Development:
manifests Generate CRDs and RBAC
generate Generate DeepCopy methods
fmt Run go fmt
vet Run go vet
test Run tests
test-e2e Run e2e tests
Build:
build Build operator binary
build-extproc Build external processor binary
build-all Build all binaries
run Run operator locally
run-extproc Run external processor locally
Docker:
docker-build Build operator image
docker-build-extproc Build external processor image
docker-build-all Build all images
docker-push Push operator image
docker-push-extproc Push external processor image
docker-push-all Push all images
Deployment:
install Install CRDs
uninstall Uninstall CRDs
deploy Deploy to cluster
undeploy Undeploy from cluster
The operator watches CustomHTTPRoute resources, compiles routing rules, and writes them to ConfigMaps in the namespace configured by --routes-configmap-namespace (default: default).
| Flag | Default | Description |
|---|---|---|
--routes-configmap-namespace |
default |
Namespace where route ConfigMaps are written |
--leader-elect |
false |
Enable leader election for HA |
--health-probe-bind-address |
:8081 |
Address for health probes |
The external processor reads route ConfigMaps and makes routing decisions for Envoy.
| Flag | Default | Description |
|---|---|---|
--addr |
:9001 |
gRPC listen address |
--target-name |
"" |
Target name to filter ConfigMaps (matches spec.targetRef.name) |
--routes-configmap-namespace |
"" |
Namespace to read ConfigMaps from (empty = all namespaces) |
--access-log |
true |
Enable access logging |
--debug |
false |
Enable debug logging |
--kubeconfig |
"" |
Path to kubeconfig (uses in-cluster config if not set) |
Important: Set
--routes-configmap-namespaceon the external processor to match the operator's--routes-configmap-namespace. This prevents stale ConfigMaps in other namespaces from causing route conflicts.
Defines routing rules for a set of hostnames. Rules are compiled into an optimized routing table stored in ConfigMaps.
| Field | Description |
|---|---|
targetRef.name |
Which external processor handles these routes |
hostnames |
List of hostnames this route applies to |
pathPrefixes |
Optional prefixes to prepend to all paths |
pathPrefixes.expandMatchTypes |
Which match types are expanded with prefixes (default: all) |
rules[].matches |
Path matching conditions |
rules[].actions |
Optional transformations (redirect, rewrite, headers) |
rules[].backendRefs |
Target services (optional if redirect action) |
Connects an external processor to Istio gateway pods by generating EnvoyFilters.
| Field | Description |
|---|---|
gatewayRef.selector |
Labels to match gateway pods |
externalProcessorRef.service |
External processor service reference |
externalProcessorRef.timeout |
gRPC connection timeout (default: "5s") |
externalProcessorRef.messageTimeout |
Message exchange timeout (default: "5s") |
catchAllRoute.hostnames |
Hostnames to generate catch-all routes for |
catchAllRoute.backendRef |
Default backend for unmatched requests |
By default, CustomHTTPRoute requires a base HTTPRoute to be configured at the Istio Gateway level. Without it, requests are rejected with 404 before reaching the external processor.
The catchAllRoute field solves this by generating an EnvoyFilter that creates virtual hosts for the specified hostnames:
spec:
catchAllRoute:
hostnames:
- example.com
- api.example.com
backendRef:
name: default-backend
namespace: default
port: 80When configured, the operator generates three EnvoyFilters:
<name>-extproc: Inserts the ext_proc filter<name>-routes: Adds dynamic routing based on ext_proc headers<name>-catchall: Creates catch-all virtual hosts for the specified hostnames
| Type | Description | Example |
|---|---|---|
PathPrefix |
Matches path prefix (default) | /api matches /api/users |
Exact |
Matches exact path | /health only matches /health |
Regex |
Go regexp syntax | ^/users/[0-9]+$ |
By default, all match types (PathPrefix, Exact, Regex) are expanded with path prefixes. You can control which types are expanded using expandMatchTypes:
pathPrefixes:
values: [es, fr]
policy: Optional
expandMatchTypes: [PathPrefix] # Only expand PathPrefix, leave Exact and Regex as-isThis can also be overridden at the rule level:
rules:
- matches:
- path: /user/me
type: Exact
backendRefs:
- name: user-service
namespace: backend
port: 8080
pathPrefixes:
policy: Optional
expandMatchTypes: [PathPrefix] # This rule won't expand Exact matchesRoutes are evaluated by priority (higher first). Default priority is 1000.
- Use high priority (e.g., 2000) for specific routes like
/health - Use low priority (e.g., 100) for catch-all routes like
/
Actions allow you to transform requests before forwarding or return immediate responses.
| Action Type | Description |
|---|---|
redirect |
Return HTTP redirect (301, 302, 307, 308). No backend needed. |
rewrite |
Rewrite path and/or hostname before forwarding |
header-set |
Set a header (overwrite if exists) |
header-add |
Add a header (append if exists) |
header-remove |
Remove a header |
rules:
- matches:
- path: /old-page
type: Exact
actions:
- type: redirect
redirect:
path: /new-page
statusCode: 301
# No backendRefs needed for redirectsrules:
- matches:
- path: /blog
actions:
- type: rewrite
rewrite:
path: /cms/blog
hostname: cms-internal.svc.cluster.local
backendRefs:
- name: cms-service
namespace: backend
port: 8080rules:
- matches:
- path: /api
actions:
- type: header-set
header:
name: X-Real-IP
value: ${client_ip}
- type: header-add
header:
name: X-Request-ID
value: ${request_id}
- type: header-remove
headerName: X-Internal-Debug
backendRefs:
- name: api-service
namespace: backend
port: 8080Variables can be used in redirect.path, rewrite.path, and header.value:
| Variable | Description |
|---|---|
${path} |
Original request path |
${host} |
Original request host |
${method} |
HTTP method (GET, POST, etc.) |
${scheme} |
Request scheme (http or https) |
${client_ip} |
Client IP from X-Forwarded-For |
${request_id} |
Request ID from X-Request-ID header |
${path.segment.N} |
Nth path segment (0-indexed) |
Copyright 2026 Freepik Company.
Licensed under the Apache License, Version 2.0. See LICENSE for details.