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

  1. Keep exactly one target on the tls service.
  2. Create separate tls services (each with its own domain/listen_port) per upstream.
  3. 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

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

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/84a5988a7b4259fd. Report an issue: GitHub.