docker/cli · error
unable to read CA cert for external CA
Error message
unable to read CA cert for external CA: %w
What it means
Wraps an os.ReadFile failure when reading the file referenced by cacert= in an --external-ca spec. The CLI reads the PEM file at parse time; any I/O error (missing, no perms) is surfaced here.
Solutions
- Confirm the path exists and is readable: 'ls -l <path>' and 'cat <path>' from the CLI's working directory.
- Use an absolute path to the PEM file.
- Verify the file is valid PEM (the next check pem.Decodes it).
Example fix
# before docker swarm init --external-ca protocol=cfssl,url=https://ca,cacert=./ca.crt # after docker swarm init --external-ca protocol=cfssl,url=https://ca,cacert=/etc/docker/external-ca.crt
Defensive patterns
Strategy: validation
Validate before calling
// Validate the cacert path is readable
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("external CA cacert unreadable: %w", err)
} Type guard
func fileReadable(path string) bool {
f, err := os.Open(path)
if err != nil {
return false
}
_ = f.Close()
return true
} Prevention
- Use absolute paths for cacert files.
- Verify readability (ls -l) from the CLI's working directory.
- Confirm the file is PEM before passing.
When it happens
Trigger: Passing 'cacert=/path/that/does/not/exist' or a path the CLI process can't read. Triggered in the parseExternalCA 'cacert' case at opts.go:196-198.
Common situations: Relative path that resolves differently than expected; missing file; permissions; SELinux/AppArmor denying read; running CLI in a different mount namespace than where the cert lives.
Related errors
- no CA information available
- CA cert for external CA must be in PEM format
- the external-ca option needs a protocol= parameter
- the external-ca option needs a url= parameter
- error reading content from
AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07).
Data as JSON: /api/errors/e498072de634ddbc.
Report an issue: GitHub.
Appendix: source
Thrown at cli/command/swarm/opts.go:198
return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
// TODO(thaJeztah): these options should not be case-insensitive.
switch strings.ToLower(key) {
case "protocol":
hasProtocol = true
if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) {
externalCA.Protocol = swarm.ExternalCAProtocolCFSSL
} else {
return nil, fmt.Errorf("unrecognized external CA protocol %s", value)
}
case "url":
hasURL = true
externalCA.URL = value
case "cacert":
cacontents, err := os.ReadFile(value)
if err != nil {
return nil, fmt.Errorf("unable to read CA cert for external CA: %w", err)
}
if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil {
return nil, errors.New("CA cert for external CA must be in PEM format")
}
externalCA.CACert = string(cacontents)
default:
externalCA.Options[key] = value
}
}
if !hasProtocol {
return nil, errors.New("the external-ca option needs a protocol= parameter")
}
if !hasURL {
return nil, errors.New("the external-ca option needs a url= parameter")
}
return &externalCA, nilView on GitHub (pinned to 4f84911bfe)