Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/01-route-registrar-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ The route-registrar expects a configuration json file like the one below:
- `health_check` is optional and explained in more detail below.
- `sni_routable_san` is the SAN used to route the request to the appropriate
backend. Required when `type` is `sni` and `terminate_frontend_tls` is enabled.
- `sni_rewrite_san` is the SAN used to override the SNI hostname sent to backend
servers during TLS handshakes. Optional, but when provided, requires `type` to be `sni` and `terminate_frontend_tls` to be enabled.
This allows backends to receive a different hostname than what the client provided.
- `terminate_frontend_tls` is optional. When true, the router will terminate
TLS before forwarding the requests to the backend servers. Default: false
- `enable_backend_tls` is optional. When true, the router will initiate a
Expand All @@ -109,11 +112,21 @@ The route registrar can be used to setup SNI routing. This is an example route j
"external_port": "TLS_PORT_OF_ROUTE_SOURCE",
"name": "SOME_ROUTE_NAME",
"sni_port": "TLS_PORT_OF_ROUTE_DESTINATION",
"router_group": "SOME_ROUTER_GROUP",
"registration_interval": "20s",
"terminate_frontend_tls": true,
"sni_routable_san": "routable.example.com",
"sni_rewrite_san": "backend.internal.hostname"
}
]
}
```

The `sni_rewrite_san` field is optional. When provided, it requires `type` to be `sni` and `terminate_frontend_tls` to be enabled.
It allows you to specify a different SNI hostname to send to backend servers than the one received
from the client. This is useful when backends require specific hostnames for certificate validation
or routing purposes.

## Health check

If the `health_check` is not configured for a route collection, the routes are continually registered according to the `registration_interval`.
Expand Down
2 changes: 2 additions & 0 deletions jobs/route_registrar/spec
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ properties:
alpns (optional, array): Application Layer Protocol Negotiation strings.
sni_routable_san(optional, string): is the SAN used to route the request to the appropriate backend.
Required when `type` is `sni` and `terminate_frontend_tls` is enabled.
sni_rewrite_san(optional, string): is the SAN used to override the SNI hostname sent to backend servers during TLS handshakes.
Optional, but when provided, requires `type` to be `sni` and `terminate_frontend_tls` to be enabled.

health_check object
name (required, string): Human-readable reference for the healthcheck
Expand Down
7 changes: 7 additions & 0 deletions jobs/route_registrar/templates/registrar_settings.json.erb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ TEXT
route['uris'].map! { |uri| "#{spec.index}-#{uri}" }
end

if route['sni_rewrite_san'] != nil && route['sni_rewrite_san'] != ''
if route['type'] != 'sni' || !route['terminate_frontend_tls']
raise "route_registrar.routes[#{index}].route.sni_rewrite_san can only be provided when type is sni and terminate_frontend_tls is enabled"
end
end

if route['type'] == 'sni' && route['terminate_frontend_tls']
if route['sni_routable_san'] == nil || route['sni_routable_san'] == ''
raise "route_registrar.routes[#{index}].route.sni_routable_san must be provided when type is sni and terminate_frontend_tls is enabled"
Expand All @@ -112,6 +118,7 @@ TEXT
end
end
end

end

host = nil
Expand Down
99 changes: 99 additions & 0 deletions spec/route_registar_templates_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"type" => "sni",
"sni_port" => 5671,
"sni_routable_san" => "svc-1.foobar.com",
"sni_rewrite_san" => "svc-1.foobar.com",
"terminate_frontend_tls" => true,
"enable_backend_tls" => true
} }
Expand Down Expand Up @@ -709,6 +710,7 @@
"type" => "sni",
"sni_port" => 5671,
"sni_routable_san" => "svc-1.foobar.com",
"sni_rewrite_san" => "svc-1.foobar.com",
"terminate_frontend_tls" => true,
"enable_backend_tls" => true
})
Expand All @@ -719,6 +721,7 @@
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0]['terminate_frontend_tls'] = false
merged_manifest_properties['route_registrar']['routes'][0].delete('sni_rewrite_san')
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

Expand All @@ -743,6 +746,102 @@
end
end

describe 'when type is sni and frontend_tls is enabled and sni_routable_san is nil' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0].delete('sni_routable_san')
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'raises an error for invalid sni_routable_san' do
expect { template.render(merged_manifest_properties, consumes: links) }.to raise_error(
RuntimeError, 'route_registrar.routes[0].route.sni_routable_san must be provided when type is sni and terminate_frontend_tls is enabled'
)
end
end

describe 'when sni_rewrite_san is provided with type sni and terminate_frontend_tls enabled' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0]['sni_rewrite_san'] = 'rewrite.example.com'
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'should use the provided sni_rewrite_san' do
rendered_hash = JSON.parse(template.render(merged_manifest_properties, consumes: links))
expect(rendered_hash['routes'][0]['sni_rewrite_san']).to eq('rewrite.example.com')
end
end

describe 'when sni_rewrite_san is provided but type is not sni' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0]['type'] = 'http'
merged_manifest_properties['route_registrar']['routes'][0]['sni_rewrite_san'] = 'rewrite.example.com'
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'raises an error for invalid sni_rewrite_san' do
expect { template.render(merged_manifest_properties, consumes: links) }.to raise_error(
RuntimeError, 'route_registrar.routes[0].route.sni_rewrite_san can only be provided when type is sni and terminate_frontend_tls is enabled'
)
end
end

describe 'when sni_rewrite_san is provided but terminate_frontend_tls is false' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0]['terminate_frontend_tls'] = false
merged_manifest_properties['route_registrar']['routes'][0]['sni_rewrite_san'] = 'rewrite.example.com'
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'raises an error for invalid sni_rewrite_san' do
expect { template.render(merged_manifest_properties, consumes: links) }.to raise_error(
RuntimeError, 'route_registrar.routes[0].route.sni_rewrite_san can only be provided when type is sni and terminate_frontend_tls is enabled'
)
end
end

describe 'when sni_rewrite_san is provided but terminate_frontend_tls is nil' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0].delete('terminate_frontend_tls')
merged_manifest_properties['route_registrar']['routes'][0]['sni_rewrite_san'] = 'rewrite.example.com'
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'raises an error for invalid sni_rewrite_san' do
expect { template.render(merged_manifest_properties, consumes: links) }.to raise_error(
RuntimeError, 'route_registrar.routes[0].route.sni_rewrite_san can only be provided when type is sni and terminate_frontend_tls is enabled'
)
end
end

describe 'when sni_rewrite_san is not provided (empty string) with type sni and terminate_frontend_tls enabled' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0]['sni_rewrite_san'] = ''
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'renders the template successfully' do
expect { template.render(merged_manifest_properties, consumes: links) }.not_to raise_error
end
end

describe 'when sni_rewrite_san is not provided (nil) with type sni and terminate_frontend_tls enabled' do
before do
merged_manifest_properties['route_registrar']['routes'][0] = sni_route
merged_manifest_properties['route_registrar']['routes'][0].delete('sni_rewrite_san')
merged_manifest_properties['nats'] = { 'fail_if_using_nats_without_tls' => false }
end

it 'renders the template successfully' do
expect { template.render(merged_manifest_properties, consumes: links) }.not_to raise_error
end
end

describe 'when tls is not enabled and the san is not provided' do
before do
merged_manifest_properties['route_registrar']['routes'][0].delete('tls_port')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ var _ = Describe("Main", func() {
ConfigFilePath: configFile,
}

tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-1", 61000, 0, "", nil, 120, models.ModificationTag{}, false, "")
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-1", 61000, 0, "", nil, nil, 120, models.ModificationTag{}, false, "")
err := routingApiClient.UpsertTcpRouteMappings([]models.TcpRouteMapping{tcpRouteMapping})
Expect(err).ToNot(HaveOccurred())

Expand All @@ -177,7 +177,7 @@ var _ = Describe("Main", func() {
It("starts an SSE connection to the server", func() {
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Subscribing-to-routing-api-event-stream"))
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Successfully-subscribed-to-routing-api-event-stream"))
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-2", 61000, 0, "", nil, 120, models.ModificationTag{}, false, "")
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-2", 61000, 0, "", nil, nil, 120, models.ModificationTag{}, false, "")
err := routingApiClient.UpsertTcpRouteMappings([]models.TcpRouteMapping{tcpRouteMapping})
Expect(err).ToNot(HaveOccurred())
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("handle-event.finished"))
Expand All @@ -192,7 +192,7 @@ var _ = Describe("Main", func() {
It("prunes stale routes", func() {
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Subscribing-to-routing-api-event-stream"))
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Successfully-subscribed-to-routing-api-event-stream"))
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-3", 61000, 0, "", nil, 6, models.ModificationTag{}, false, "")
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-3", 61000, 0, "", nil, nil, 6, models.ModificationTag{}, false, "")
err := routingApiClient.UpsertTcpRouteMappings([]models.TcpRouteMapping{tcpRouteMapping})
Expect(err).ToNot(HaveOccurred())
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("handle-event.finished"))
Expand Down Expand Up @@ -258,7 +258,7 @@ var _ = Describe("Main", func() {
Eventually(session.Out, time.Second).Should(gbytes.Say("syncer.stopping"))
})
It("continues to process events", func() {
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-2", 61000, 61001, "instance-id", nil, 120, models.ModificationTag{}, false, "")
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-2", 61000, 61001, "instance-id", nil, nil, 120, models.ModificationTag{}, false, "")
err := routingApiClient.UpsertTcpRouteMappings([]models.TcpRouteMapping{tcpRouteMapping})
Expect(err).ToNot(HaveOccurred())

Expand Down Expand Up @@ -399,7 +399,7 @@ var _ = Describe("Main", func() {
server = routingApiServer(logger)
routerGroupGuid = getRouterGroupGuid(routingApiClient)
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("Successfully-subscribed-to-routing-api-event-stream"))
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-3", 61000, 0, "", nil, 120, models.ModificationTag{}, false, "")
tcpRouteMapping := models.NewTcpRouteMapping(routerGroupGuid, 5222, "some-ip-3", 61000, 0, "", nil, nil, 120, models.ModificationTag{}, false, "")
err := routingApiClient.UpsertTcpRouteMappings([]models.TcpRouteMapping{tcpRouteMapping})
Expect(err).ToNot(HaveOccurred())
Eventually(session.Out, 5*time.Second).Should(gbytes.Say("handle-event.finished"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func (cm configMarshaller) marshalHAProxyBackend(backendName string, backend mod
if server.InstanceID != "" {
output.WriteString(fmt.Sprintf(" verifyhost %s", server.InstanceID))
}

if enableFrontendTLS && server.SniRewriteHostname != "" {
output.WriteString(fmt.Sprintf(" sni str(%s)", server.SniRewriteHostname))
}
} else {
if server.TLSPort == 0 && backendTlsCfg.Enabled {
cm.logger.Error("route-missing-tls-information", fmt.Errorf("Backend TLSPort was set to 0. If TLS is intentionally off for this backend, set this to -1 to suppress this message"), lager.Data{"backend": server})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,49 @@ backend backend_80

})
})

Context("when SniRewriteHostname is provided", func() {
It("does not configure the backend server with sni str directive when frontend TLS is disabled", func() {
haproxyConf = models.HAProxyConfig{
80: {
"": {
{Address: "host-88.internal", Port: 8888, TLSPort: 8443, InstanceID: "host-88-instance-id", SniRewriteHostname: "rewrite.example.com"},
},
},
}
Expect(marshaller.Marshal(haproxyConf, backendTlsCfg, frontendTlsCfg)).To(Equal(`
frontend frontend_80
mode tcp
bind :80
default_backend backend_80

backend backend_80
mode tcp
server server_host-88.internal_8443 host-88.internal:8443 ssl verify required ca-file /fake/path/to/ca.pem verifyhost host-88-instance-id
`))
})

It("configures the backend server with sni str directive when frontend TLS is enabled", func() {
frontendTlsCfg[0].Enabled = true
haproxyConf = models.HAProxyConfig{
80: {
"": {
{Address: "host-88.internal", Port: 8888, TLSPort: 8443, InstanceID: "host-88-instance-id", SniRewriteHostname: "rewrite.example.com", TerminateFrontendTLS: true},
},
},
}
Expect(marshaller.Marshal(haproxyConf, backendTlsCfg, frontendTlsCfg)).To(Equal(`
frontend frontend_80
mode tcp
bind :80 ssl crt /fake/path/to/certs/
default_backend backend_80

backend backend_80
mode tcp
server server_host-88.internal_8443 host-88.internal:8443 ssl verify required ca-file /fake/path/to/ca.pem verifyhost host-88-instance-id sni str(rewrite.example.com)
`))
})
})
})
Context("when TLSPort is 0", func() {
It("Logs an error indicating that the backend is not being encrypted", func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type BackendServerInfo struct {
TTL int
TerminateFrontendTLS bool
ALPNs string
SniRewriteHostname string
}

type BackendServerKey struct {
Expand All @@ -34,6 +35,7 @@ type BackendServerKey struct {
InstanceID string
TerminateFrontendTLS bool
ALPNs string
SniRewriteHostname string
}

type BackendServerDetails struct {
Expand All @@ -56,7 +58,7 @@ func NewRoutingTableEntry(backends []BackendServerInfo) RoutingTableEntry {
Backends: make(map[BackendServerKey]BackendServerDetails),
}
for _, backend := range backends {
backendServerKey := BackendServerKey{Address: backend.Address, Port: backend.Port, TLSPort: backend.TLSPort, InstanceID: backend.InstanceID, TerminateFrontendTLS: backend.TerminateFrontendTLS, ALPNs: backend.ALPNs}
backendServerKey := BackendServerKey{Address: backend.Address, Port: backend.Port, TLSPort: backend.TLSPort, InstanceID: backend.InstanceID, TerminateFrontendTLS: backend.TerminateFrontendTLS, ALPNs: backend.ALPNs, SniRewriteHostname: backend.SniRewriteHostname}
backendServerDetails := BackendServerDetails{ModificationTag: backend.ModificationTag, TTL: backend.TTL, UpdatedTime: time.Now()}

routingTableEntry.Backends[backendServerKey] = backendServerDetails
Expand Down Expand Up @@ -128,7 +130,7 @@ func (table RoutingTable) PruneEntries(defaultTTL int) {
}

func (table RoutingTable) serverKeyDetailsFromInfo(info BackendServerInfo) (BackendServerKey, BackendServerDetails) {
return BackendServerKey{Address: info.Address, Port: info.Port, TLSPort: info.TLSPort, InstanceID: info.InstanceID, TerminateFrontendTLS: info.TerminateFrontendTLS, ALPNs: info.ALPNs}, BackendServerDetails{ModificationTag: info.ModificationTag, TTL: info.TTL, UpdatedTime: time.Now()}
return BackendServerKey{Address: info.Address, Port: info.Port, TLSPort: info.TLSPort, InstanceID: info.InstanceID, TerminateFrontendTLS: info.TerminateFrontendTLS, ALPNs: info.ALPNs, SniRewriteHostname: info.SniRewriteHostname}, BackendServerDetails{ModificationTag: info.ModificationTag, TTL: info.TTL, UpdatedTime: time.Now()}
}

// Set returns true if routing configuration should be modified, false if it should not.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ func (u *updater) toRoutingTableEntry(logger lager.Logger, routeMapping apimodel
hostname = *routeMapping.SniHostname
}

var sniRewriteHostname string
if routeMapping.SniRewriteHostname != nil {
sniRewriteHostname = *routeMapping.SniRewriteHostname
}

routingKey := models.RoutingKey{
Port: routeMapping.ExternalPort,
SniHostname: models.SniHostname(hostname),
Expand All @@ -253,6 +258,7 @@ func (u *updater) toRoutingTableEntry(logger lager.Logger, routeMapping apimodel
TTL: ttl,
TerminateFrontendTLS: routeMapping.TerminateFrontendTLS,
ALPNs: routeMapping.ALPNs,
SniRewriteHostname: sniRewriteHostname,
}
return routingKey, backendServerInfo
}
Expand Down
Loading