tailscale/tailscale · error

register: %w

Error message

register: %w

What it means

obtainCert's call to acme.Client.Register (create/reuse the ACME account) failed with an error other than ErrAccountAlreadyExists (which is tolerated). The %w wraps a golang.org/x/crypto/acme error: unreachable directory URL, TLS/CA-root failure to the ACME endpoint, HTTP 400 for invalid account/contact, or 5xx from the CA.

Source

Thrown at cmd/derper/ipcert.go:389

		}
		m.mu.Unlock()
	}
}

// obtainCert does one ACME issuance flow for ip: it registers the
// account if needed, orders a short-lived profile certificate for the
// IP address identifier, fulfills the HTTP-01 challenges, and installs
// and caches the issued certificate.
func (m *ipCertManager) obtainCert(ctx context.Context, ip netip.Addr) error {
	ipStr := ip.String()

	var contact []string
	if m.email != "" {
		contact = []string{"mailto:" + m.email}
	}
	_, err := m.client.Register(ctx, &acme.Account{Contact: contact}, acme.AcceptTOS)
	if err != nil && !errors.Is(err, acme.ErrAccountAlreadyExists) {
		return fmt.Errorf("register: %w", err)
	}

	order, err := m.client.AuthorizeOrder(ctx, acme.IPIDs(ipStr), acme.WithOrderProfile(shortlivedProfile))
	if err != nil {
		return fmt.Errorf("new order: %w", err)
	}
	for _, authzURL := range order.AuthzURLs {
		if err := m.fulfillAuthz(ctx, authzURL); err != nil {
			return err
		}
	}
	order, err = m.client.WaitOrder(ctx, order.URI)
	if err != nil {
		return fmt.Errorf("waiting for order: %w", err)
	}

	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Verify egress: curl https://acme-v02.api.letsencrypt.org/directory from the derper host.
  2. Install CA roots in containers (apt-get install ca-certificates or equivalent).
  3. Validate the email/contact argument is a plain address, not mailto:.
  4. If the account key is suspect, delete acme-account.key to force a clean registration.

Example fix

docker run --rm derper-image curl -sS https://acme-v02.api.letsencrypt.org/directory
# before: minimal distroless image -> register: tls: failed to verify certificate
# after: add CA certs to the image
RUN apk add --no-cache ca-certificates
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight ACME endpoint reachability and CA roots before starting.
func acmeReachable(dirURL string) error {
    cl := &http.Client{Timeout: 10 * time.Second}
    resp, err := cl.Get(dirURL)
    if err != nil { return err }
    resp.Body.Close()
    if resp.StatusCode >= 500 { return fmt.Errorf("acme directory %s: %d", dirURL, resp.StatusCode) }
    return nil
}

Type guard

func isACMEError(err error) bool { var e *acme.Error; return errors.As(err, &e) }

Try / catch

err := mgr.ObtainCert(ctx, ip)
var ae *acme.Error
if errors.As(err, &ae) && ae.StatusCode/100 == 5 {
    // CA-side transient: back off and retry later
} else if errors.As(err, &ae) && ae.StatusCode == 400 {
    // bad contact/account: fix config, do not retry
}

Prevention

When it happens

Trigger: newIPCertManager configured (possibly with a custom directoryURL) then certForIP triggers the first issuance: Register POSTs to the CA. Fails when egress to acme-v02.api.letsencrypt.org is blocked, the account key on disk was corrupted/replaced mid-life, the email contact is rejected, or the container lacks CA root certificates.

Common situations: Minimal container images without ca-certificates; firewall/proxy blocking outbound HTTPS; malformed --acme-email; tests pointing directoryURL at a fake ACME server that is not running.

Related errors


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