tailscale/tailscale · error
ACME account key: %w
Error message
ACME account key: %w
What it means
Returned by newIPCertManager when loadOrCreateAccountKey cannot read, parse, or create the ACME account key at <certdir>/acme-account.key. The %w wraps the underlying cause: invalid PEM, x509.ParseECPrivateKey failure (key is not ECDSA P-256 material), or a filesystem error creating a new key (EACCES, read-only volume, disk full).
Source
Thrown at cmd/derper/ipcert.go:91
flight chan struct{} // non-nil while an issuance is running; closed when it finishes
flightErr error // result of the last finished issuance
nextAttempt time.Time // earliest time of the next issuance attempt, after a failure
retryDelay time.Duration // backoff to apply after the next failure
}
// newIPCertManager returns an ipCertManager storing its ACME account
// key and issued certificates in certdir.
//
// If directoryURL is empty, the LetsEncrypt production directory is
// used; tests point it at a fake ACME server. If next is non-nil,
// connections with a DNS name in the SNI are served by it.
func newIPCertManager(certdir, email, directoryURL string, next certProvider) (*ipCertManager, error) {
if err := os.MkdirAll(certdir, 0700); err != nil {
return nil, err
}
accountKey, err := loadOrCreateAccountKey(filepath.Join(certdir, "acme-account.key"))
if err != nil {
return nil, fmt.Errorf("ACME account key: %w", err)
}
m := &ipCertManager{
certDir: certdir,
email: email,
client: &acme.Client{
Key: accountKey,
DirectoryURL: directoryURL,
UserAgent: "tailscale-derper",
},
next: next,
certs: make(map[netip.Addr]*ipCertEntry),
tokens: make(map[string]string),
}
if next != nil {
m.nextTLS = next.TLSConfig()
}
go m.renewLoop()
return m, nilView on GitHub (pinned to cfe32b8be6)
Solutions
- Read the wrapped error to distinguish corrupt key (pem/parse errors) from permissions (os permission denied).
- If the key file is corrupt or foreign, delete <certdir>/acme-account.key; derper generates a new P-256 key and re-registers with the ACME CA automatically.
- Fix ownership/permissions: chown the certdir to the derper user and keep it 0700.
- Ensure the volume is writable and has space if running in Docker/Kubernetes.
Example fix
# before: corrupt/foreign account key blocks startup rm -f /var/lib/derper/acme-account.key # if regenerating is acceptable chown -R derper: /var/lib/derper && chmod 700 /var/lib/derper systemctl restart derper
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight the cert dir before constructing the IP cert manager.
func checkCertDir(dir string) error {
if err := os.MkdirAll(dir, 0700); err != nil { return err }
f, err := os.OpenFile(filepath.Join(dir, ".probe"), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { return fmt.Errorf("certdir not writable: %w", err) }
f.Close()
os.Remove(filepath.Join(dir, ".probe"))
return nil
} Try / catch
m, err := newIPCertManager(certdir, email, directoryURL, next)
if err != nil {
// unwrapped %w tells you: pem/parse => bad key file, permission => dir perms
log.Fatalf("ip cert manager: %v", err)
} Prevention
- Keep the ACME certdir writable by the derper user and 0700.
- Persist the certdir across restarts (no ephemeral /tmp for prod).
- Do not copy account files from other ACME clients into derper's certdir.
- Alert on derper startup failures so a bad account key is caught at deploy time.
When it happens
Trigger: Starting derper with IP cert mode (newIPCertManager) where certdir exists but acme-account.key is unreadable or corrupt, where the file holds a non-EC key (e.g. RSA or a JWK dumped by another ACME tool), or where the process lacks write permission to generate a fresh key. Also triggers when certdir is on a read-only mount (some container setups).
Common situations: Reusing a certdir previously written by certbot or another ACME client; running derper as a different user than the one that owns the directory; containers with a read-only or non-persistent volume; disk full at first launch.
Related errors
- invalid PEM in %s
- no certificate for hostname %q; this server only serves IP a
- cert issuance for %v failed recently; next attempt no earlie
- cached cert is expired or not yet valid (NotAfter %v)
- register: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/973d3c8dc7ed96a4.
Report an issue: GitHub.