caddyserver/caddy · error
subject does not qualify for certificate: '%s'
Error message
subject does not qualify for certificate: '%s'
What it means
While splitting automation policy subjects into internal vs public issuer groups, each subject (except Tailscale domains) is checked with certmagic.SubjectQualifiesForCert. Names that cannot be in any certificate — empty strings, names with spaces, IPs with a port, a bare '*', over-253-char names, dotless names where disallowed — are rejected here during adaptation.
Source
Thrown at caddyconfig/httpcaddyfile/tlsapp.go:255
// associate our new automation policy with this server block's hosts
ap.SubjectsRaw = hostsNotHTTP
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
// need to separate them out in the automation policies so
// that the internal names can use the internal issuer and
// the other names can use the default/public/ACME issuer
var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 {
var internal, external []string
for _, s := range ap.SubjectsRaw {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
if isTailscaleDomain(s) {
continue
}
if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
}
// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
// names like *.*.tld that may not qualify for a public certificate are actually
// fine when used with OnDemand, since OnDemand (currently) does not obtain
// wildcards (if it ever does, there will be a separate config option to enable
// it that we would need to check here) since the hostname is known at handshake;
// and it is unexpected to switch to internal issuer when the user wants to get
// regular certificates on-demand for a class of certs like *.*.tld.
if subjectQualifiesForPublicCert(ap, s) {
external = append(external, s)
} else {
internal = append(internal, s)
}
}
if len(external) > 0 && len(internal) > 0 {
ap.SubjectsRaw = external
apCopy := *ap
ap2 = &apCopyView on GitHub (pinned to 50e54ee279)
Solutions
- Fix the reported subject (printed in quotes in the error) to be a valid DNS name or IP without port/scheme/spaces.
- If you need an internal-only name, ensure it still qualifies (use 'foo.internal' — not empty, not bare '*', no port).
- Check what your placeholders/env vars actually expand to at adapt time.
Example fix
# before
https://example.com:8443 {
tls internal
}
# after
example.com:8443 {
tls internal
} Defensive patterns
Strategy: validation
Prevention
- Use bare hostnames (no scheme, no port) as site addresses; let Caddy derive the TLS subject.
- Lint generated/host-provisioned hostnames for emptiness, spaces, ports, and double dots.
When it happens
Trigger: A site address or tls subject like 'my site.com' (space), 'localhost:3000' leaking the port into the TLS subject, a bare '*' label, 'bad..name' double dots, or a malformed placeholder-expanded hostname.
Common situations: Env-var/placeholder-expanded hostnames coming out empty or malformed; copying URLs (with scheme or port) instead of bare hostnames into site addresses; typos creating double dots or trailing spaces.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- consolidating TLS connection policies for server %d: %v
- server listening on %v is HTTP, but attempts to configure TL
- two policies with same match criteria have conflicting ALPN:
- two policies with same match criteria have conflicting ciphe
- two policies with same match criteria have conflicting clien
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/ad46e59647613a3e.
Report an issue: GitHub.