Skip to content

[client] Add client-side support for mTLS.#5416

Open
reimarstier wants to merge 2 commits intonetbirdio:mainfrom
mercedes-benz:424-mtls-for-netbird-client
Open

[client] Add client-side support for mTLS.#5416
reimarstier wants to merge 2 commits intonetbirdio:mainfrom
mercedes-benz:424-mtls-for-netbird-client

Conversation

@reimarstier
Copy link
Copy Markdown

@reimarstier reimarstier commented Feb 23, 2026

This PR adds client support for sending a certificate to management/signal/relay backend for mutual authentication (mTLS).

Background

We've been using NetBird in eclipse-opendut for over two years now and are grateful for the VPN management that it provides. For internet facing services, we are required to use client certificates (mutual TLS aka mTLS). At the moment we use NetBird behind traefik and are able to configure client certificate verification here.

Changes

I would like to add support for client certificates in the NetBird client.

This pull request adds two fields to the configuration input:

// client/internal/profilemanager/config.go
type ConfigInput struct {

	// IDPClientCert holds the mTLS cert/key paths for OAuth PKCE Authorization Flow with the identity provider (SSO)
	IDPClientCert MTLSConfig
	// MgmtClientCert holds the mTLS cert/key paths for connecting to management/signal/relay backend
	MgmtClientCert MTLSConfig

}

// client/internal/profilemanager/config_mtls.go
type MTLSConfig struct {
	CertPath string           `json:",omitempty"`
	KeyPath  string           `json:",omitempty"`
	KeyPair  *tls.Certificate `json:"-"`
}

If you configure a certificate path and key path, then the NetBird client will send the client certificate when making requests to the backend (management service, relay service, signal service).
There are already configuration fields present to configure client certificates for the identity provider, contributed in #2188. To avoid breaking existing configuration, I've opted for a descriptive longer name in the configuration.
Let me know if this is to your liking.

type Config struct {

	// IDPClientCert holds the mTLS cert/key paths for OAuth PKCE Authorization Flow with the identity provider (SSO)
	IDPClientCert MTLSConfig

	// MgmtClientCert holds the mTLS cert/key paths for connecting to management/signal/relay backend
	MgmtClientCert MTLSConfig

	// Deprecated: use IDPClientCert.CertPath instead. Kept for reading legacy config files.
	ClientCertPath string `json:",omitempty"`
	// Deprecated: use IDPClientCert.KeyPath instead. Kept for reading legacy config files.
	ClientCertKeyPath string `json:",omitempty"`
}

I have already run tests for v0.67.1:

  • Custom releases
  • CI Tests.
    Apart from that I've tested integration within our backend. I haven't tested on other operating systems than linux x86_64.

There are others who have requested more general support for client certificates and a PKI: mTLS Auth for Proxy Services.
Adding client-side support is a first step in that direction.

Checklist

  • Is a feature enhancement

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Legal notice:
Reimar Stier, reimar.stier@mercedes-benz.com, Mercedes-Benz Tech Innovation GmbH, provider information

Summary by CodeRabbit

  • New Features

    • Optional mutual TLS (mTLS) client certificate support for management, signal, and relay connections; native clients can present a client certificate when configured.
    • Configuration accepts management client certificate/key paths and will load/cache a client certificate when both paths are provided; WASM/browser builds ignore client certs.
  • Tests

    • Tests and test helpers updated to accept the new optional certificate parameter.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Threads an optional management client TLS certificate through configuration, mgmt/signal/relay client constructors, gRPC/WebSocket dialers, and relay manager to optionally enable mTLS when a certificate is provided.

Changes

Cohort / File(s) Summary
gRPC dial & clients
client/grpc/dialer.go, shared/management/client/grpc.go, shared/signal/client/grpc.go
CreateConnection and NewClient signatures extended to accept *tls.Certificate; gRPC dialing builds a tls.Config and conditionally attaches the client cert for mTLS.
Configuration / profile manager
client/internal/profilemanager/config.go
New config fields MgmtClientCertPath, MgmtClientKeyPath and cached MgmtClientCertKeyPair *tls.Certificate; loads X.509 key pair on apply and exposes it to callers.
Client plumbing (auth/connect/server)
client/internal/auth/auth.go, client/internal/connect.go, client/server/server.go
Propagate MgmtClientCertKeyPair through mgm.NewClient, signal/relay constructors and connect flows; updated function signatures and call sites.
Relay client core & manager
shared/relay/client/client.go, shared/relay/client/manager.go, shared/relay/client/picker.go
Added clientCert *tls.Certificate fields; extended NewClient/NewManager signatures to accept and store client cert; propagate cert when creating/opening relay connections.
WebSocket dialer & dial options
shared/relay/client/dialer/ws/ws.go, shared/relay/client/dialer/ws/dialopts_generic.go, shared/relay/client/dialer/ws/dialopts_js.go
WS Dialer now carries ClientCert; HTTP client builder constructs tls.Config with RootCAs and optionally attaches client certificate; JS/WASM path accepts but ignores client cert.
Relay dialer selection
shared/relay/client/dialers_generic.go, shared/relay/client/dialers_js.go
getDialers now instantiates websocket dialer with ClientCert set (possibly nil); JS build documents browser-controlled TLS (mTLS unsupported).
Tests & call-site updates
client/internal/engine_test.go, shared/relay/client/manager_test.go, relay/test/benchmark_test.go, relay/testec2/relay.go, shared/management/client/client_test.go, shared/relay/client/client_test.go
Updated many tests and helpers to pass an extra trailing argument (commonly nil) to match expanded constructor signatures across mgmt/signal/relay APIs.

Sequence Diagram(s)

sequenceDiagram
    participant ProfileMgr as ProfileManager
    participant Engine as Client Engine
    participant RelayMgr as Relay Manager/Picker
    participant Dialer as nbgrpc/WS Dialer
    participant Server as Management/Signal Server

    ProfileMgr->>Engine: provide MgmtClientCertKeyPair (*tls.Certificate)
    Engine->>RelayMgr: NewManager / NewClient (pass clientCert)
    RelayMgr->>Dialer: CreateConnection / Dial (addr, tlsEnabled, component, mgmtClientCert)
    Note right of Dialer: build tls.Config with RootCAs\nattach mgmtClientCert if non-nil
    Dialer->>Server: establish TLS / mTLS connection
    Server-->>Engine: TLS handshake / gRPC or WS established
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • pappz
  • pascal-fischer
  • mlsmaycon

Poem

🐰 I found a little cert so bright,
I tucked it in configs by moonlight,
I hopped from mgr to dial and back,
mTLS now threads each network track,
Secure hops make my whiskers light.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main feature addition: client-side support for mTLS, which aligns with the changeset's primary objective.
Description check ✅ Passed PR description is comprehensive, detailed, and well-structured with clear context, motivation, and implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
shared/signal/client/grpc.go (1)

57-62: Consider renaming mgmtClientCert to clientCert in the signal client.

The parameter is named mgmtClientCert here, but this is the signal client — the certificate serves the same mTLS purpose for signal connections, not management specifically. Using a generic name like clientCert (as done in the relay client) would be clearer and more consistent.

The same naming concern applies to CreateConnection in client/grpc/dialer.go where the parameter is also called mgmtClientCert.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/signal/client/grpc.go` around lines 57 - 62, Rename the parameter
mgmtClientCert to clientCert for clarity and consistency: update NewClient(ctx
context.Context, addr string, key wgtypes.Key, tlsEnabled bool, mgmtClientCert
*tls.Certificate) to use clientCert and change all usages within NewClient
(including the nbgrpc.CreateConnection call) to pass clientCert; likewise update
the CreateConnection signature and its implementation in the dialer from
mgmtClientCert to clientCert and adjust any local variable names,
documentation/comments, and call sites to match the new identifier (e.g.,
references in nbgrpc.CreateConnection, dialer.go, and other callers).
client/internal/profilemanager/config.go (2)

573-591: Consider warning when only one of cert/key path is provided, and failing hard on load errors.

Two observations:

  1. If a user sets MgmtClientCertPath but not MgmtClientKeyPath (or vice versa), nothing happens and there's no warning — the incomplete configuration is silently ignored. A log warning would help troubleshoot misconfiguration.

  2. When tls.LoadX509KeyPair fails (line 584-586), the error is only logged and execution continues. The client will then silently connect without mTLS, which may violate the user's security expectations. Consider returning an error from apply() in this case, or at minimum logging at Warn level instead of Error (since the current Error log suggests something is wrong but doesn't prevent the insecure fallback).

I recognize both patterns are consistent with the existing ClientCertPath handling above (lines 563-571), so this could be addressed as a follow-up for both cases.

Proposed improvement
+	if (input.MgmtClientCertPath != "") != (input.MgmtClientKeyPath != "") {
+		log.Warnf("only one of MgmtClientCertPath/MgmtClientKeyPath is set; both are required for mTLS")
+	}
+
 	if config.MgmtClientCertPath != "" && config.MgmtClientKeyPath != "" {
 		cert, err := tls.LoadX509KeyPair(config.MgmtClientCertPath, config.MgmtClientKeyPath)
 		if err != nil {
-			log.Error("Failed to load mTLS cert/key pair for management: ", err)
+			return false, fmt.Errorf("failed to load mTLS cert/key pair for management: %w", err)
 		} else {
 			config.MgmtClientCertKeyPair = &cert
 			log.Info("Loaded client mTLS cert/key pair for management.")
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/profilemanager/config.go` around lines 573 - 591, Detect and
warn on incomplete mTLS config and fail on load errors: when only one of
config.MgmtClientCertPath or config.MgmtClientKeyPath is set, emit a warning log
(e.g., log.Warn) so the user knows the config is incomplete; when both are set
and tls.LoadX509KeyPair(config.MgmtClientCertPath, config.MgmtClientKeyPath)
returns an error, do not silently continue — return that error from apply() (or
the enclosing function) instead of merely logging, and only set
config.MgmtClientCertKeyPair on success; apply the same
warning/return-on-load-error pattern used for ClientCertPath/ClientKeyPath
handling above so behavior is consistent.

163-166: Verify that MgmtClientCertPath and MgmtClientKeyPath are not logged or written to sensitive locations.

The certificate and key file paths will be persisted to the JSON config file (only the MgmtClientCertKeyPair field has json:"-"). This is fine for paths, but worth confirming that the key file itself has appropriate permissions. Consider documenting the expected file permissions for the key file (e.g., 0600).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/profilemanager/config.go` around lines 163 - 166, The
MgmtClientCertPath and MgmtClientKeyPath fields must not be logged or written to
insecure places and the key file itself must have restrictive permissions; add a
comment beside MgmtClientKeyPath documenting the required permission (e.g.,
0600) and update the config load/validate path (where the struct is parsed or
saved) to (1) avoid emitting these values to logs or any exported JSON (keep
MgmtClientCertKeyPair json:"-"), (2) validate that the file at MgmtClientKeyPath
exists and has mode 0600 (or stricter) and return an error otherwise, and (3)
ensure any config serialization/Save function explicitly omits or redacts the
key path when writing to shared locations or logs; reference the
MgmtClientCertPath, MgmtClientKeyPath, and MgmtClientCertKeyPair fields when
making these checks and updates.
client/grpc/dialer.go (1)

46-51: Log message says "management" but this function serves both management and signal connections.

CreateConnection is called for both management (via wsproxy.ManagementComponent) and signal (via wsproxy.SignalComponent) connections. The log messages on lines 47 and 50 reference "management" specifically, which would be misleading when establishing a signal connection.

Consider using the component parameter in the log message for clarity:

♻️ Suggested fix
 		if mgmtClientCert != nil {
-			log.Debugf("Using client certificate for management connection. mTLS enabled.")
+			log.Debugf("Using client certificate for %s connection. mTLS enabled.", component)
 			tlsConfig.Certificates = []tls.Certificate{*mgmtClientCert}
 		} else {
-			log.Debugf("No client certificate for management provided. Disabling mTLS.")
+			log.Debugf("No client certificate provided for %s connection. mTLS disabled.", component)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/grpc/dialer.go` around lines 46 - 51, The log messages in
CreateConnection hardcode "management" but this function is used for both
management and signal connections; update the Debugf calls that reference
"management" (around the mgmtClientCert and tlsConfig handling) to include the
function's component parameter (e.g., component or
wsproxy.ManagementComponent/wsproxy.SignalComponent) so the logs reflect which
connection is being established; ensure both the "Using client certificate..."
and "No client certificate..." messages incorporate the component variable for
accurate, non-misleading logging.
client/internal/connect.go (1)

219-219: Consider renaming config fields to reflect use across all backends, not just management.

The configuration field MgmtClientCertKeyPair is intentionally passed to all three backends—management (line 219), signal (line 268), and relay (line 294)—as confirmed by the config documentation stating: "Path to client certificate used for mTLS authentication that is used to connect to management/signal/relay backend."

However, the field name and parameter names (mgmtClientCert) across signal and management clients are misleading since the certificate serves all three services, not just management. This inconsistency is compounded by relay using a generic ClientCert naming.

Renaming to ClientCertKeyPair (without the "Mgmt" prefix) in the config and updating corresponding parameter names would improve clarity without changing functionality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/connect.go` at line 219, Rename the config field and
parameter names that imply "Mgmt" to a generic name used by all backends: change
MgmtClientCertKeyPair to ClientCertKeyPair in the config struct and docs, update
any references and constructor parameters (e.g., the mgm.NewClient call that
currently passes c.config.MgmtClientCertKeyPair, the signal client constructor
that accepts mgmtClientCert, and the relay client usage of ClientCert) to use
the new ClientCertKeyPair name, and rename parameter identifiers (e.g.,
mgmtClientCert -> clientCert) in function signatures for mgm.NewClient,
signal.NewClient and relay.NewClient so names consistently reflect the shared
client certificate across management/signal/relay backends.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shared/relay/client/dialers_generic.go`:
- Around line 16-18: The QUIC dialer isn't given the mTLS client certificate so
QUIC handshakes fail when mTLS is required; update the quic.Dialer type to
accept a ClientCert (mirror ws.Dialer.ClientCert), set quic.Dialer{ClientCert:
c.clientCert} where the race dialer is returned, and ensure the
ClientQUICTLSConfig (or the TLS config builder used by quic.Dialer) uses that
ClientCert when building the tls.Config so QUIC supports mTLS; alternatively, if
you prefer not to change quic.Dialer, explicitly document that mTLS forces
WS-only transport and keep the existing behavior.

---

Nitpick comments:
In `@client/grpc/dialer.go`:
- Around line 46-51: The log messages in CreateConnection hardcode "management"
but this function is used for both management and signal connections; update the
Debugf calls that reference "management" (around the mgmtClientCert and
tlsConfig handling) to include the function's component parameter (e.g.,
component or wsproxy.ManagementComponent/wsproxy.SignalComponent) so the logs
reflect which connection is being established; ensure both the "Using client
certificate..." and "No client certificate..." messages incorporate the
component variable for accurate, non-misleading logging.

In `@client/internal/connect.go`:
- Line 219: Rename the config field and parameter names that imply "Mgmt" to a
generic name used by all backends: change MgmtClientCertKeyPair to
ClientCertKeyPair in the config struct and docs, update any references and
constructor parameters (e.g., the mgm.NewClient call that currently passes
c.config.MgmtClientCertKeyPair, the signal client constructor that accepts
mgmtClientCert, and the relay client usage of ClientCert) to use the new
ClientCertKeyPair name, and rename parameter identifiers (e.g., mgmtClientCert
-> clientCert) in function signatures for mgm.NewClient, signal.NewClient and
relay.NewClient so names consistently reflect the shared client certificate
across management/signal/relay backends.

In `@client/internal/profilemanager/config.go`:
- Around line 573-591: Detect and warn on incomplete mTLS config and fail on
load errors: when only one of config.MgmtClientCertPath or
config.MgmtClientKeyPath is set, emit a warning log (e.g., log.Warn) so the user
knows the config is incomplete; when both are set and
tls.LoadX509KeyPair(config.MgmtClientCertPath, config.MgmtClientKeyPath) returns
an error, do not silently continue — return that error from apply() (or the
enclosing function) instead of merely logging, and only set
config.MgmtClientCertKeyPair on success; apply the same
warning/return-on-load-error pattern used for ClientCertPath/ClientKeyPath
handling above so behavior is consistent.
- Around line 163-166: The MgmtClientCertPath and MgmtClientKeyPath fields must
not be logged or written to insecure places and the key file itself must have
restrictive permissions; add a comment beside MgmtClientKeyPath documenting the
required permission (e.g., 0600) and update the config load/validate path (where
the struct is parsed or saved) to (1) avoid emitting these values to logs or any
exported JSON (keep MgmtClientCertKeyPair json:"-"), (2) validate that the file
at MgmtClientKeyPath exists and has mode 0600 (or stricter) and return an error
otherwise, and (3) ensure any config serialization/Save function explicitly
omits or redacts the key path when writing to shared locations or logs;
reference the MgmtClientCertPath, MgmtClientKeyPath, and MgmtClientCertKeyPair
fields when making these checks and updates.

In `@shared/signal/client/grpc.go`:
- Around line 57-62: Rename the parameter mgmtClientCert to clientCert for
clarity and consistency: update NewClient(ctx context.Context, addr string, key
wgtypes.Key, tlsEnabled bool, mgmtClientCert *tls.Certificate) to use clientCert
and change all usages within NewClient (including the nbgrpc.CreateConnection
call) to pass clientCert; likewise update the CreateConnection signature and its
implementation in the dialer from mgmtClientCert to clientCert and adjust any
local variable names, documentation/comments, and call sites to match the new
identifier (e.g., references in nbgrpc.CreateConnection, dialer.go, and other
callers).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44ef1a1 and 657db57.

📒 Files selected for processing (21)
  • client/grpc/dialer.go
  • client/internal/auth/auth.go
  • client/internal/connect.go
  • client/internal/engine_test.go
  • client/internal/profilemanager/config.go
  • client/server/server.go
  • relay/test/benchmark_test.go
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/relay/client/client.go
  • shared/relay/client/client_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • shared/relay/client/dialer/ws/dialopts_js.go
  • shared/relay/client/dialer/ws/ws.go
  • shared/relay/client/dialers_generic.go
  • shared/relay/client/dialers_js.go
  • shared/relay/client/manager.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/picker.go
  • shared/signal/client/grpc.go

Comment thread shared/relay/client/dialers_generic.go
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from 657db57 to 84c8c85 Compare February 26, 2026 16:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/grpc/dialer.go (1)

45-51: Log message is overly specific for a generic function.

The CreateConnection function is used for multiple components (management, signal) as indicated by the component parameter, but the log messages specifically reference "management connection" and "management provided". This could cause confusion when reviewing logs for signal connections.

Consider making the log messages generic or including the component name:

♻️ Suggested improvement
 		// Only add client certificate if provided
 		if mgmtClientCert != nil {
-			log.Debugf("Using client certificate for management connection. mTLS enabled.")
+			log.Debugf("Using client certificate for %s connection. mTLS enabled.", component)
 			tlsConfig.Certificates = []tls.Certificate{*mgmtClientCert}
 		} else {
-			log.Debugf("No client certificate for management provided. Disabling mTLS.")
+			log.Debugf("No client certificate provided for %s. mTLS disabled.", component)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/grpc/dialer.go` around lines 45 - 51, The log messages in
CreateConnection are too specific to "management" while the function is generic;
update the log.Debugf calls that reference "management connection" and
"management provided" to either use the component parameter (e.g., include
component name) or make them generic (e.g., "Using client certificate for
connection" / "No client certificate provided, disabling mTLS"), leaving the
existing behavior that sets tlsConfig.Certificates from mgmtClientCert
unchanged; modify the calls where mgmtClientCert is checked and where
tlsConfig.Certificates is set so log messages reference component or are
generic, and ensure you adjust both debug branches consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/internal/profilemanager/config.go`:
- Around line 573-591: The current block that attempts to load the management
mTLS keypair (checking config.MgmtClientCertPath and config.MgmtClientKeyPath
and calling tls.LoadX509KeyPair) only logs errors and continues; change it to
fail fast by returning an error when tls.LoadX509KeyPair returns an error, and
ensure you clear any in-memory config.MgmtClientCertKeyPair (set it to nil)
before returning so no unusable keypair remains; update the surrounding function
signature to propagate the error up (return the error from the function that
handles config updates) and on success keep the existing assignment of
config.MgmtClientCertKeyPair and the info log.

---

Nitpick comments:
In `@client/grpc/dialer.go`:
- Around line 45-51: The log messages in CreateConnection are too specific to
"management" while the function is generic; update the log.Debugf calls that
reference "management connection" and "management provided" to either use the
component parameter (e.g., include component name) or make them generic (e.g.,
"Using client certificate for connection" / "No client certificate provided,
disabling mTLS"), leaving the existing behavior that sets tlsConfig.Certificates
from mgmtClientCert unchanged; modify the calls where mgmtClientCert is checked
and where tlsConfig.Certificates is set so log messages reference component or
are generic, and ensure you adjust both debug branches consistently.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 657db57 and 84c8c85.

📒 Files selected for processing (21)
  • client/grpc/dialer.go
  • client/internal/auth/auth.go
  • client/internal/connect.go
  • client/internal/engine_test.go
  • client/internal/profilemanager/config.go
  • client/server/server.go
  • relay/test/benchmark_test.go
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/relay/client/client.go
  • shared/relay/client/client_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • shared/relay/client/dialer/ws/dialopts_js.go
  • shared/relay/client/dialer/ws/ws.go
  • shared/relay/client/dialers_generic.go
  • shared/relay/client/dialers_js.go
  • shared/relay/client/manager.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/picker.go
  • shared/signal/client/grpc.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • client/server/server.go
  • shared/relay/client/client_test.go
  • shared/signal/client/grpc.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/client.go

Comment thread client/internal/profilemanager/config.go Outdated
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from 84c8c85 to c192683 Compare February 26, 2026 16:23
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
client/internal/profilemanager/config.go (1)

573-581: Consider skipping updates when management cert/key paths are unchanged.

updated is set to true whenever non-empty input is present, even if the value is identical. This can trigger unnecessary config rewrites and cert reloads.

Suggested refinement
-	if input.MgmtClientKeyPath != "" {
+	if input.MgmtClientKeyPath != "" && input.MgmtClientKeyPath != config.MgmtClientKeyPath {
 		config.MgmtClientKeyPath = input.MgmtClientKeyPath
 		updated = true
 	}

-	if input.MgmtClientCertPath != "" {
+	if input.MgmtClientCertPath != "" && input.MgmtClientCertPath != config.MgmtClientCertPath {
 		config.MgmtClientCertPath = input.MgmtClientCertPath
 		updated = true
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/profilemanager/config.go` around lines 573 - 581, The code
unconditionally sets updated=true when input.MgmtClientKeyPath or
input.MgmtClientCertPath are non-empty, causing needless rewrites; change the
logic in the block that touches config.MgmtClientKeyPath and
config.MgmtClientCertPath to only assign and set updated=true when the incoming
value differs from the existing one (compare input.MgmtClientKeyPath !=
config.MgmtClientKeyPath and input.MgmtClientCertPath !=
config.MgmtClientCertPath before assigning). Ensure you use the same variable
names (input.MgmtClientKeyPath, config.MgmtClientKeyPath,
input.MgmtClientCertPath, config.MgmtClientCertPath, updated) so unchanged paths
do not flip the updated flag.
shared/relay/client/client_test.go (1)

71-71: Add one positive mTLS test case.

These updates validate only the nil certificate path. Please add at least one test that passes a non-nil *tls.Certificate and verifies cert plumbing is actually exercised.

Also applies to: 79-79, 87-87, 147-147, 187-187, 230-230, 242-242, 322-322, 370-370, 376-376, 398-398, 473-473, 479-479, 537-537, 543-543, 593-593, 651-651, 704-704, 716-716

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/relay/client/client_test.go` at line 71, Add a positive mTLS test that
passes a real non-nil *tls.Certificate into NewClient to exercise cert plumbing:
generate or load a test cert/key pair (e.g., tls.X509KeyPair with PEM blobs or a
helper that creates a self-signed cert), pass &tlsCert into NewClient (where
code currently calls NewClient(..., nil)), perform an actual dial/handshake
against the test server (reuse serverCfg/StartTestServer or the existing test
harness), then assert the server observed the client certificate (e.g., check
server side ConnectionState.PeerCertificates or a stubbed
TLSConfig.GetConfigForClient/GetCertificate was invoked) and that the client
connection succeeded; apply this change to the NewClient calls referenced in the
test (the instances around the given lines) so each nil-certificate case has at
least one corresponding non-nil-certificate positive test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/grpc/dialer.go`:
- Around line 47-50: The debug messages in dialer.go hardcode "management
connection" — update them to be component-agnostic by using a neutral phrase
(e.g., "connection" or include a passed-in connectionName/connType variable if
available) so the logs apply to all dialer usages; specifically change the two
log.Debugf calls near tlsConfig.Certificates and the else branch that reference
"management" to use the generic wording, keeping the existing behavior that sets
tlsConfig.Certificates = []tls.Certificate{*mgmtClientCert} when mgmtClientCert
is present.

---

Nitpick comments:
In `@client/internal/profilemanager/config.go`:
- Around line 573-581: The code unconditionally sets updated=true when
input.MgmtClientKeyPath or input.MgmtClientCertPath are non-empty, causing
needless rewrites; change the logic in the block that touches
config.MgmtClientKeyPath and config.MgmtClientCertPath to only assign and set
updated=true when the incoming value differs from the existing one (compare
input.MgmtClientKeyPath != config.MgmtClientKeyPath and input.MgmtClientCertPath
!= config.MgmtClientCertPath before assigning). Ensure you use the same variable
names (input.MgmtClientKeyPath, config.MgmtClientKeyPath,
input.MgmtClientCertPath, config.MgmtClientCertPath, updated) so unchanged paths
do not flip the updated flag.

In `@shared/relay/client/client_test.go`:
- Line 71: Add a positive mTLS test that passes a real non-nil *tls.Certificate
into NewClient to exercise cert plumbing: generate or load a test cert/key pair
(e.g., tls.X509KeyPair with PEM blobs or a helper that creates a self-signed
cert), pass &tlsCert into NewClient (where code currently calls NewClient(...,
nil)), perform an actual dial/handshake against the test server (reuse
serverCfg/StartTestServer or the existing test harness), then assert the server
observed the client certificate (e.g., check server side
ConnectionState.PeerCertificates or a stubbed
TLSConfig.GetConfigForClient/GetCertificate was invoked) and that the client
connection succeeded; apply this change to the NewClient calls referenced in the
test (the instances around the given lines) so each nil-certificate case has at
least one corresponding non-nil-certificate positive test.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84c8c85 and c192683.

📒 Files selected for processing (21)
  • client/grpc/dialer.go
  • client/internal/auth/auth.go
  • client/internal/connect.go
  • client/internal/engine_test.go
  • client/internal/profilemanager/config.go
  • client/server/server.go
  • relay/test/benchmark_test.go
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/relay/client/client.go
  • shared/relay/client/client_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • shared/relay/client/dialer/ws/dialopts_js.go
  • shared/relay/client/dialer/ws/ws.go
  • shared/relay/client/dialers_generic.go
  • shared/relay/client/dialers_js.go
  • shared/relay/client/manager.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/picker.go
  • shared/signal/client/grpc.go
✅ Files skipped from review due to trivial changes (1)
  • shared/relay/client/dialers_js.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • shared/relay/client/client.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • client/internal/auth/auth.go
  • relay/test/benchmark_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • client/server/server.go

Comment thread client/grpc/dialer.go Outdated
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from c192683 to f1615f2 Compare February 26, 2026 16:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
shared/relay/client/manager_test.go (1)

26-34: Consider adding test coverage for the mTLS path.

All existing tests pass nil for the new clientCert parameter. Consider adding at least one test that exercises the mTLS code path with a non-nil certificate to verify the feature works end-to-end through the manager layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/relay/client/manager_test.go` around lines 26 - 34, Add a new unit
test (or extend TestEmptyURL) that exercises the mTLS path by passing a non-nil
clientCert into NewManager so the code path that handles client certificates is
exercised; create a test TLS certificate using tls.X509KeyPair or load a small
test cert/key pair (e.g., from testdata) and pass that certificate as the
clientCert argument to NewManager, then call Serve() and assert expected
behavior (error or success) to verify the manager uses the certificate
end-to-end through the Serve method.
client/internal/profilemanager/config.go (1)

563-571: Consider aligning SSO certificate error handling with the new fail-fast pattern.

The existing SSO client certificate loading (lines 563-571) only logs errors and continues, while the new management certificate loading (lines 583-595) fails fast on errors. This inconsistency could lead to silent failures for SSO mTLS.

Consider applying the same fail-fast pattern here for consistency:

♻️ Suggested refactor
-	if config.ClientCertPath != "" && config.ClientCertKeyPath != "" {
-		cert, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientCertKeyPath)
-		if err != nil {
-			log.Error("Failed to load mTLS cert/key pair: ", err)
-		} else {
-			config.ClientCertKeyPair = &cert
-			log.Info("Loaded client mTLS cert/key pair")
-		}
-	}
+	config.ClientCertKeyPair = nil
+	if (config.ClientCertPath == "") != (config.ClientCertKeyPath == "") {
+		return updated, fmt.Errorf("both ClientCertPath and ClientCertKeyPath must be set together")
+	}
+	if config.ClientCertPath != "" {
+		cert, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientCertKeyPath)
+		if err != nil {
+			return updated, fmt.Errorf("failed to load SSO mTLS cert/key pair: %w", err)
+		}
+		config.ClientCertKeyPair = &cert
+		log.Info("Loaded client mTLS cert/key pair for SSO")
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/profilemanager/config.go` around lines 563 - 571, The SSO
mTLS block currently swallows tls.LoadX509KeyPair errors (using log.Error)
whereas the management cert code fails fast; change the SSO block so that on
error from tls.LoadX509KeyPair(config.ClientCertPath, config.ClientCertKeyPath)
you fail fast in the same way the management cert handling does (i.e., return
the error from the enclosing function or call the same fatal/logging path used
for management certs) instead of just logging, and still set
config.ClientCertKeyPair = &cert and log.Info("Loaded client mTLS cert/key
pair") on success; update callers if needed to propagate the returned error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/internal/profilemanager/config.go`:
- Around line 563-571: The SSO mTLS block currently swallows tls.LoadX509KeyPair
errors (using log.Error) whereas the management cert code fails fast; change the
SSO block so that on error from tls.LoadX509KeyPair(config.ClientCertPath,
config.ClientCertKeyPath) you fail fast in the same way the management cert
handling does (i.e., return the error from the enclosing function or call the
same fatal/logging path used for management certs) instead of just logging, and
still set config.ClientCertKeyPair = &cert and log.Info("Loaded client mTLS
cert/key pair") on success; update callers if needed to propagate the returned
error.

In `@shared/relay/client/manager_test.go`:
- Around line 26-34: Add a new unit test (or extend TestEmptyURL) that exercises
the mTLS path by passing a non-nil clientCert into NewManager so the code path
that handles client certificates is exercised; create a test TLS certificate
using tls.X509KeyPair or load a small test cert/key pair (e.g., from testdata)
and pass that certificate as the clientCert argument to NewManager, then call
Serve() and assert expected behavior (error or success) to verify the manager
uses the certificate end-to-end through the Serve method.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c192683 and f1615f2.

📒 Files selected for processing (21)
  • client/grpc/dialer.go
  • client/internal/auth/auth.go
  • client/internal/connect.go
  • client/internal/engine_test.go
  • client/internal/profilemanager/config.go
  • client/server/server.go
  • relay/test/benchmark_test.go
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/relay/client/client.go
  • shared/relay/client/client_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • shared/relay/client/dialer/ws/dialopts_js.go
  • shared/relay/client/dialer/ws/ws.go
  • shared/relay/client/dialers_generic.go
  • shared/relay/client/dialers_js.go
  • shared/relay/client/manager.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/picker.go
  • shared/signal/client/grpc.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • shared/relay/client/client.go
  • shared/relay/client/manager.go
  • relay/test/benchmark_test.go
  • shared/signal/client/grpc.go
  • shared/relay/client/dialer/ws/dialopts_js.go

@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from f1615f2 to 5897b4b Compare February 27, 2026 07:59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
client/internal/engine_test.go (1)

1512-1517: Consider adding one positive-path test with a non-nil client certificate.

Current updates validate API compatibility (nil cert), but a single test that exercises non-nil cert plumbing would better protect the new mTLS path.

Also applies to: 1548-1548

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/engine_test.go` around lines 1512 - 1517, Add a positive-path
unit test in engine_test.go that exercises the mTLS client-cert plumbing by
calling mgmt.NewClient and signal.NewClient with a non-nil client certificate
instead of nil; create or reuse a test TLS client certificate (e.g., a helper
that returns *tls.Certificate or x509 cert + key), pass it as the certificate
argument to mgmt.NewClient and signal.NewClient, assert no error is returned and
the returned client is non-nil, and optionally verify that the client attempts
an mTLS handshake (mock server/assert on dial options) to ensure the non-nil
cert path is exercised; reference mgmt.NewClient and signal.NewClient when
adding the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/internal/engine_test.go`:
- Around line 1512-1517: Add a positive-path unit test in engine_test.go that
exercises the mTLS client-cert plumbing by calling mgmt.NewClient and
signal.NewClient with a non-nil client certificate instead of nil; create or
reuse a test TLS client certificate (e.g., a helper that returns
*tls.Certificate or x509 cert + key), pass it as the certificate argument to
mgmt.NewClient and signal.NewClient, assert no error is returned and the
returned client is non-nil, and optionally verify that the client attempts an
mTLS handshake (mock server/assert on dial options) to ensure the non-nil cert
path is exercised; reference mgmt.NewClient and signal.NewClient when adding the
test.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1615f2 and 5897b4b.

📒 Files selected for processing (21)
  • client/grpc/dialer.go
  • client/internal/auth/auth.go
  • client/internal/connect.go
  • client/internal/engine_test.go
  • client/internal/profilemanager/config.go
  • client/server/server.go
  • relay/test/benchmark_test.go
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/relay/client/client.go
  • shared/relay/client/client_test.go
  • shared/relay/client/dialer/ws/dialopts_generic.go
  • shared/relay/client/dialer/ws/dialopts_js.go
  • shared/relay/client/dialer/ws/ws.go
  • shared/relay/client/dialers_generic.go
  • shared/relay/client/dialers_js.go
  • shared/relay/client/manager.go
  • shared/relay/client/manager_test.go
  • shared/relay/client/picker.go
  • shared/signal/client/grpc.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • relay/testec2/relay.go
  • shared/management/client/client_test.go
  • shared/signal/client/grpc.go
  • shared/management/client/grpc.go
  • shared/relay/client/dialer/ws/ws.go
  • client/internal/profilemanager/config.go
  • shared/relay/client/manager_test.go

@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch 2 times, most recently from 184783a to b56a472 Compare March 6, 2026 10:47
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch 3 times, most recently from 3b96023 to a28441b Compare March 17, 2026 12:33
@reimarstier
Copy link
Copy Markdown
Author

Hi @mlsmaycon, I kindly asked for a review. Is there a maintainer group that I can tag? I'd like to have feedback on the following:

  • I've updated the config field names in the latest commit and extracted it to a struct. Is this something you would accept?
type Config struct {

	// IDPClientCert holds the mTLS cert/key paths for OAuth PKCE Authorization Flow with the identity provider (SSO)
	IDPClientCert MTLSConfig

	// MgmtClientCert holds the mTLS cert/key paths for connecting to management/signal/relay backend
	MgmtClientCert MTLSConfig

	// Deprecated: use IDPClientCert.CertPath instead. Kept for reading legacy config files.
	ClientCertPath string `json:",omitempty"`
	// Deprecated: use IDPClientCert.KeyPath instead. Kept for reading legacy config files.
	ClientCertKeyPath string `json:",omitempty"`
}

This renames ClientCertPath and ClientCertKeyPath while keeping a clear migration compatible fallback. This allows applying the same logic to both client certificate configurations in applyMTLSCertKeyPair.

  • Are the configuration field names to your liking?

@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from a28441b to 5355884 Compare March 18, 2026 13:01
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch 3 times, most recently from 0d8429f to 8f67a91 Compare March 27, 2026 11:06
@reimarstier
Copy link
Copy Markdown
Author

@lixmal Can you take another look?

@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from 8f67a91 to 5a437f4 Compare March 31, 2026 05:24
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch 4 times, most recently from 08d418d to da1622b Compare April 17, 2026 06:33
Client supports sending certificate to management/signal/relay backend for mutual authentication.
@reimarstier reimarstier force-pushed the 424-mtls-for-netbird-client branch from da1622b to 543b4b5 Compare April 17, 2026 08:36
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant