netbirdio/netbird · error
TLS services must have exactly one target
Error message
TLS services must have exactly one target
What it means
Returned by validateTLSMode when len(s.Targets) != 1. A TLS service passes the decrypted-byte stream (post SNI match) to exactly one upstream; multiple targets have no routing key at this layer. Same single-target contract as tcp/udp, and for the same reason.
Source
Thrown at management/internals/modules/reverseproxy/service/service.go:947
}
if s.Mode == ModeUDP && s.Targets[0].ProxyProtocol {
return errors.New("proxy_protocol is not supported for UDP services")
}
return s.validateL4Target(s.Targets[0])
}
func (s *Service) validateTLSMode() error {
if s.Domain == "" {
return errors.New("domain is required for TLS services (used for SNI matching)")
}
if s.isAuthEnabled() {
return errors.New("auth is not supported for TLS services")
}
if s.ListenPort == 0 {
return errors.New("listen_port is required for TLS services")
}
if len(s.Targets) != 1 {
return errors.New("TLS services must have exactly one target")
}
return s.validateL4Target(s.Targets[0])
}
func (s *Service) validateHTTPTargets() error {
for i, target := range s.Targets {
switch target.TargetType {
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
// Host is normally overwritten by replaceHostByLookup with the
// resolved peer IP / resource address; operator-supplied values
// are honored only when DirectUpstream is set. Validate the
// override here so misconfigured hosts fail fast at API time.
if err := validateDirectUpstreamHost(i, target); err != nil {
return err
}
case TargetTypeSubnet:
if target.Host == "" {
return fmt.Errorf("target %d has empty host but target_type is %q", i, target.TargetType)View on GitHub (pinned to 93e97f4bf1)
Solutions
- Keep exactly one target on the tls service.
- Create separate tls services (each with its own domain/listen_port) per upstream.
- If multiple backends behind one hostname are the goal, use mode http where multi-target path routing exists.
Example fix
// before
{ "mode": "tls", "listen_port": 4433, "targets": [
{ "target_id": "peer-a", "port": 443 }, { "target_id": "peer-b", "port": 443 } ] }
// after
{ "mode": "tls", "listen_port": 4433, "targets": [ { "target_id": "peer-a", "port": 443 } ] } Defensive patterns
Strategy: validation
Validate before calling
func checkTLSSingleTarget(mode string, targets []Target) error {
if mode == "tls" && len(targets) != 1 {
return fmt.Errorf("tls needs exactly 1 target, got %d", len(targets))
}
return nil
} Type guard
func isTLSTargetCountValid(mode string, targets []Target) bool {
return mode != "tls" || len(targets) == 1
} Try / catch
if err := svc.Validate(); err != nil {
if strings.Contains(err.Error(), "TLS services must have exactly one target") {
return respondBadRequest(errors.New("one tls service per upstream"))
}
return respondBadRequest(err)
} Prevention
- Treat all L4 modes (tcp/udp/tls) as single-upstream contracts.
- Generate one service object per upstream in automation rather than appending targets.
- Validate target cardinality client-side per mode to catch it before the round trip.
When it happens
Trigger: Submitting a tls service with two or more targets; reusing an HTTP multi-target template for a TLS passthrough service.
Common situations: Listing a primary and standby database behind one TLS endpoint expecting failover. Path-routed HTTP habits carried into tls mode.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- TCP/UDP services must have exactly one target
- auth is not supported for TLS services
- target_id is required for L4 services
- target port is required for L4 services
- target host is required for subnet targets
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/84a5988a7b4259fd.
Report an issue: GitHub.