netbirdio/netbird · error

both cert-file and cert-key must be provided when not using

Error message

both cert-file and cert-key must be provided when not using LetsEncrypt

What it means

The signal server's TLS setup requires certificate material before it can create gRPC transport credentials. When --letsencrypt-domain is empty, the code demands both --cert-file and --cert-key; if either flag is missing or empty it logs and returns this error, so `signal run` aborts before the listener starts. It is a deliberate startup validation, not a runtime failure.

Source

Thrown at signal/cmd/run.go:231

		tlsConfig   *tls.Config
	)

	if signalLetsencryptDomain == "" && signalCertFile == "" && signalCertKey == "" {
		log.Infof("running without TLS")
		return nil, nil, nil, nil
	}

	if signalLetsencryptDomain != "" {
		certManager, err = encryption.CreateCertManager(signalLetsencryptDataDir, signalLetsencryptDomain)
		if err != nil {
			return nil, certManager, nil, err
		}
		tlsConfig = certManager.TLSConfig()
		log.Infof("setting up TLS with LetsEncrypt.")
	} else {
		if signalCertFile == "" || signalCertKey == "" {
			log.Errorf("both cert-file and cert-key must be provided when not using LetsEncrypt")
			return nil, certManager, nil, errors.New("both cert-file and cert-key must be provided when not using LetsEncrypt")
		}

		tlsConfig, err = loadTLSConfig(signalCertFile, signalCertKey)
		if err != nil {
			log.Errorf("cannot load TLS credentials: %v", err)
			return nil, certManager, nil, err
		}
		log.Infof("setting up TLS with custom certificates.")
	}

	transportCredentials := credentials.NewTLS(tlsConfig)

	return []grpc.ServerOption{grpc.Creds(transportCredentials)}, certManager, tlsConfig, err
}

func startServerWithCertManager(certManager *autocert.Manager, grpcRootHandler http.Handler) {
	// a call to certManager.Listener() always creates a new listener so we do it once
	httpListener := certManager.Listener()

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Pass both flags: --cert-file /path/tls.crt --cert-key /path/tls.key with valid PEM files.
  2. Or switch to LetsEncrypt: --letsencrypt-domain signal.example.com with --letsencrypt-data-dir writable.
  3. Verify the flags actually reach the process (inspect `ps aux` or the unit file) and that both mounted files are non-empty.

Example fix

# before
signal run --log-level debug

# after (custom certs)
signal run --cert-file /etc/certs/tls.crt --cert-key /etc/certs/tls.key

# or (LetsEncrypt)
signal run --letsencrypt-domain signal.example.com --letsencrypt-data-dir /var/lib/signal/certs
Defensive patterns

Strategy: validation

Validate before calling

// Before starting the signal server, validate the TLS flag combination
if letsencryptDomain == "" && (certFile == "" || certKey == "") {
    return errors.New("provide both --cert-file and --cert-key, or set --letsencrypt-domain")
}

Try / catch

if err := runServer(...); err != nil {
    if strings.Contains(err.Error(), "both cert-file and cert-key") {
        // config problem: fix flags, do not retry
    }
    return err
}

Prevention

When it happens

Trigger: Running `signal run` with neither --letsencrypt-domain nor the cert pair; supplying only one of --cert-file / --cert-key; one flag resolving to an empty string because a docker-compose env var or systemd unit referenced a secret that did not mount.

Common situations: Migrating a signal deployment from plain HTTP to TLS and forgetting the key flag; Kubernetes/Docker secret mounting only one of the two PEM files; copying a compose template that omits both flags.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/ef6b39c056b0fcbf. Report an issue: GitHub.