caddyserver/caddy · error

protocol min (%x) cannot be greater than protocol max (%x)

Error message

protocol min (%x) cannot be greater than protocol max (%x)

What it means

Returned by buildStandardTLSConfig when both protocol bounds are set and the textual ProtocolMin compares greater than ProtocolMax. Versions are compared as strings, which works only because the supported names (tls1.0...tls1.3) sort lexicographically in version order; the intended order in config is min first, max second.

Source

Thrown at modules/caddytls/connpolicy.go:371

	// add all the curve preferences in order, without duplicates
	curvesAdded := make(map[tls.CurveID]struct{})
	for _, curveName := range p.Curves {
		curveID := SupportedCurves[curveName]
		if _, ok := curvesAdded[curveID]; !ok {
			curvesAdded[curveID] = struct{}{}
			cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
		}
	}

	// ensure ALPN includes the ACME TLS-ALPN protocol
	alpnFound := slices.Contains(p.ALPN, acmez.ACMETLS1Protocol)
	if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) {
		cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol)
	}

	// min and max protocol versions
	if (p.ProtocolMin != "" && p.ProtocolMax != "") && p.ProtocolMin > p.ProtocolMax {
		return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
	}
	if p.ProtocolMin != "" {
		cfg.MinVersion = SupportedProtocols[p.ProtocolMin]
	}
	if p.ProtocolMax != "" {
		cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
	}

	// client authentication
	if p.ClientAuthentication != nil {
		if err := p.ClientAuthentication.provision(ctx); err != nil {
			return fmt.Errorf("provisioning client CA: %v", err)
		}
		if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil {
			return fmt.Errorf("configuring TLS client authentication: %v", err)
		}

		// Prevent privilege escalation in case multiple vhosts are configured for

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Swap the values so the lower version comes first: 'protocol_version tls1.2 tls1.3'
  2. Or set only one bound if a single limit was intended
  3. Re-validate with 'caddy validate --config <file>'

Example fix

# before
tls {
	protocol_version tls1.3 tls1.2
}

# after
tls {
	protocol_version tls1.2 tls1.3
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate ordering before building config:
func validProtocolRange(min, max string) bool {
	if min == "" || max == "" {
		return true
	}
	return min <= max // works for tls1.0..tls1.3 lexicographic order
}

Prevention

When it happens

Trigger: Caddyfile 'protocol_version tls1.3 tls1.2' (arguments reversed); JSON protocol_min="tls1.3", protocol_max="tls1.2"; hand-edited configs swapping the two fields.

Common situations: Users assuming newest-first ordering from other software, or editing the pair manually and inverting them. The %x verbs even render the values oddly (hex-encoded strings), but the names remain readable enough to spot the inversion.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/5685fa2f589332dd. Report an issue: GitHub.