gastownhall/beads · error

uow: external TLS: %w

Error message

uow: external TLS: %w

What it means

NewExternalDoltServerUOWProvider wraps any failure from registerExternalTLSConfig as "uow: external TLS". That helper only runs when external.TLSRequired is set; it either fails to build a tls.Config from the ExternalDoltConfig (via external.TLSClientConfig()) or fails to register it with the go-sql-driver mysql package. It is a configuration/data problem with the TLS material, not a network failure — the connection hasn't been attempted yet.

Source

Thrown at internal/storage/uow/external_doltserver_provider.go:56

	if rootUser == "" {
		return nil, fmt.Errorf("uow: rootUser must not be empty")
	}
	if err := external.Validate(); err != nil {
		return nil, fmt.Errorf("uow: external: %w", err)
	}

	absServerRootDir, err := filepath.Abs(serverRootDir)
	if err != nil {
		return nil, fmt.Errorf("uow: resolving server root dir: %w", err)
	}

	if err := os.MkdirAll(absServerRootDir, config.BeadsDirPerm); err != nil {
		return nil, fmt.Errorf("uow: creating server root directory: %w", err)
	}

	tlsConfigName, err := registerExternalTLSConfig(external)
	if err != nil {
		return nil, fmt.Errorf("uow: external TLS: %w", err)
	}

	ep, err := proxy.GetCreateDatabaseProxyServerEndpoint(absServerRootDir, proxy.OpenOpts{
		Backend:     proxy.BackendExternal,
		LogFilePath: serverLogFilePath,
		External:    external,
		IdleTimeout: idleTimeout,
		Port:        proxyPort,
	})
	if err != nil {
		return nil, fmt.Errorf("uow: get proxy endpoint: %w", err)
	}

	return openAndInitSchema(ctx, ep, database, rootUser, rootPassword, tlsConfigName, teamServer, expectedProjectID, applyProviderOptions(opts))
}

func registerExternalTLSConfig(external configfile.ExternalDoltConfig) (string, error) {
	if !external.TLSRequired {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify every TLS file path in the ExternalDoltConfig is absolute and readable by the process (ls/stat the TLSCACert, TLSCert, TLSKey files).
  2. Validate the CA PEM parses (openssl x509 -in ca.pem) and the cert/key pair matches (openssl x509 -noout -modulus).
  3. If TLS is not actually required by the server, unset TLSRequired so registerExternalTLSConfig is skipped.
  4. Re-run config validation (external.Validate()) before constructing the provider to catch earlier misconfigurations.

Example fix

// before
external := configfile.ExternalDoltConfig{Host: "db.internal", Port: 3307, TLSRequired: true, TLSCACert: "ca.pem"} // relative path, wrong cwd
// after
caPath, _ := filepath.Abs("/etc/beads/tls/ca.pem")
if _, err := os.Stat(caPath); err != nil { /* fail fast with a clear message */ }
external := configfile.ExternalDoltConfig{Host: "db.internal", Port: 3307, TLSRequired: true, TLSCACert: caPath, TLSServerName: "db.internal"}
Defensive patterns

Strategy: validation

Validate before calling

func validateTLSMaterial(ext configfile.ExternalDoltConfig) error {
	if !ext.TLSRequired {
		return nil
	}
	for _, p := range []string{ext.TLSCACert, ext.TLSCert, ext.TLSKey} {
		if p != "" {
			if _, err := os.Stat(p); err != nil {
				return fmt.Errorf("TLS file %s: %w", p, err)
			}
		}
	}
	return ext.Validate()
}

Type guard

func tlsFilesReadable(ext configfile.ExternalDoltConfig) bool {
	for _, p := range []string{ext.TLSCACert, ext.TLSCert, ext.TLSKey} {
		if p != "" && !fileReadable(p) {
			return false
		}
	}
	return true
}

Try / catch

provider, err := uow.NewExternalDoltServerUOWProvider(ctx, root, db, logPath, ext, user, pw, port, 0, false, "")
if err != nil {
	if strings.Contains(err.Error(), "uow: external TLS:") {
		log.Fatalf("TLS config invalid: check TLSCACert/TLSCert/TLSKey paths and contents: %v", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling NewExternalDoltServerUOWProvider with an ExternalDoltConfig where TLSRequired=true and: TLSCACert points to a missing/unreadable file, the CA PEM contains no parseable certificates, TLSCert/TLSKey are missing or invalid (tls.LoadX509KeyPair fails), or mysql.RegisterTLSConfig rejects the config name.

Common situations: Typo'd or relative paths for TLSCACert/TLSCert/TLSKey in config.yaml that don't resolve from the process working directory; server rotated to a CA file that doesn't exist locally; client cert/key file permissions deny read; empty or malformed PEM exported from a secrets manager.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3cffc25a18f11d35. Report an issue: GitHub.