tailscale/tailscale · error
can not load x509 key pair for hostname %q: %w
Error message
can not load x509 key pair for hostname %q: %w
What it means
In --certmode=manual, NewManualCertManager builds <certdir>/<keyname>.crt and .key (keyname = --hostname with unsafe characters stripped by regex) and calls tls.LoadX509KeyPair on them. This error wraps that failure after the only recovery path: if the files don't exist AND the hostname parses as an IP, a self-signed cert is generated; anything else (missing files for a DNS name, malformed PEM, key/cert mismatch, unreadable files) surfaces here.
Source
Thrown at cmd/derper/cert.go:131
noHostname bool // whether hostname is an IP address
}
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
func NewManualCertManager(certdir, hostname string) (certProvider, error) {
keyname := unsafeHostnameCharacters.ReplaceAllString(hostname, "")
crtPath := filepath.Join(certdir, keyname+".crt")
keyPath := filepath.Join(certdir, keyname+".key")
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
hostnameIP := net.ParseIP(hostname) // or nil if hostname isn't an IP address
if err != nil {
// If the hostname is an IP address, automatically create a
// self-signed certificate for it.
var certp *tls.Certificate
if os.IsNotExist(err) && hostnameIP != nil {
certp, err = createSelfSignedIPCert(crtPath, keyPath, hostname)
}
if err != nil {
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
}
cert = *certp
}
// ensure hostname matches with the certificate
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, fmt.Errorf("can not load cert: %w", err)
}
if err := x509Cert.VerifyHostname(hostname); err != nil {
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
}
if hostnameIP != nil {
// If the hostname is an IP address, print out information on how to
// confgure this in the derpmap.
dn := &tailcfg.DERPNode{
Name: "custom",
RegionID: 900,
HostName: hostname,View on GitHub (pinned to cfe32b8be6)
Solutions
- Name the files exactly <hostname>.crt and <hostname>.key inside --certdir (characters like * are stripped from the name)
- Verify the pair loads: openssl x509 -in <hostname>.crt -noout && openssl pkey -in <hostname>.key -noout, and check modulus match
- Point --certdir at the directory actually containing the files
- If the hostname is an IP, ensure --certdir is writable so the self-signed cert can be created
- Or switch to --certmode=letsencrypt to avoid file management
Example fix
# before: derper --certmode=manual --certdir=/letsencrypt/live/derp.example.com # (dir holds fullchain.pem / privkey.pem) # after: cp fullchain.pem /certs/derp.example.com.crt # cp privkey.pem /certs/derp.example.com.key # derper --certmode=manual --certdir=/certs --hostname=derp.example.com
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight a manual cert dir before starting derper:
func checkManualCert(dir, hostname string) error {
key := regexp.MustCompile(`[^a-zA-Z0-9.-]`).ReplaceAllString(hostname, "")
if _, err := tls.LoadX509KeyPair(filepath.Join(dir, key+".crt"), filepath.Join(dir, key+".key")); err != nil {
return fmt.Errorf("manual cert pair invalid: %w", err)
}
return nil
} Prevention
- Automate deployment of <hostname>.crt/.key with exact naming into --certdir
- Verify pairs with openssl (parse + modulus match) in CI
- For IP hostnames, ensure certdir is writable to get the auto self-signed cert
- Consider letsencrypt mode to remove file management
When it happens
Trigger: Cert files not named after --hostname (e.g. fullchain.pem instead of derp.example.com.crt); --certdir pointing at the wrong directory; malformed or truncated PEM; private key not matching the certificate; file permissions; IP hostname where self-signing also failed (e.g. unwritable certdir).
Common situations: Following Let's Encrypt layouts (cert.pem/privkey.pem) instead of derper's expected naming; copying certs with the wrong case or including '*.' which gets stripped; certdir read-only so the IP self-sign fallback fails.
Related errors
- missing required --certdir flag
- --certmode=gcp requires --hostname to be a DNS name, not an
- --hostname is an IP address; use --certmode=manual for a sel
- unsupport cert mode: %q
- cert invalid for hostname %q: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/5b9f1f4670152546.
Report an issue: GitHub.