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, nil

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Read the wrapped error to distinguish corrupt key (pem/parse errors) from permissions (os permission denied).
  2. 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.
  3. Fix ownership/permissions: chown the certdir to the derper user and keep it 0700.
  4. 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

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


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/973d3c8dc7ed96a4. Report an issue: GitHub.