Skip to content

feat: add mTLS support#2514

Merged
SkArchon merged 14 commits intomainfrom
milinda/mTLS
Feb 24, 2026
Merged

feat: add mTLS support#2514
SkArchon merged 14 commits intomainfrom
milinda/mTLS

Conversation

@SkArchon
Copy link
Copy Markdown
Contributor

@SkArchon SkArchon commented Feb 16, 2026

This PR adds the following capabilities for communication between the router and subgraph.

  • Allow mTLS by providing authenticating itself to the subgraph by presenting a client certificate
  • Allow defining a root CA set which can be used to verify subgraph certificates that might not use the common CA's found on OS roots.
  • Allow insecure skip ca verification (useful for testing)

The below config is an example configuration

tls:
  client:
    all: # applies to all subgraphs
      cert_file: /path/to/client.crt
      key_file: /path/to/client.key
      ca_file: /path/to/ca.crt
    subgraphs: # subgraph specific
      products:
        cert_file: /path/to/products-client.crt
        key_file: /path/to/products-client.key
        ca_file: /path/to/ca.crt # override root cas on the router
      employees:
        insecure_skip_ca_verification: true # for testing only

Note: There already exists tls.server where the router is the "server", thus I went with the tls.client naming as in this case the router is the "client" when communicating with subgraphs.

Summary by CodeRabbit

  • New Features

    • Added mutual TLS (mTLS) for outbound connections to subgraphs with per-subgraph certificate support and global defaults
    • Per-subgraph TLS overrides and an option to skip CA verification for development
  • Configuration

    • Configuration schema and example config updated to include subgraph TLS settings (global and per-subgraph)
  • Tests

    • Added integration and unit tests covering mTLS, cert validation, and error cases with TLS-enabled test servers

Checklist

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 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

Adds per-subgraph TLS/mTLS support for outbound connections, including configuration types and schema, TLS assembly logic, router/transport plumbing to apply TLS configs, test-server TLS support, unit tests for TLS builders, and integration tests validating subgraph mTLS behaviors.

Changes

Cohort / File(s) Summary
Configuration types & schema
router/pkg/config/config.go, router/pkg/config/config.schema.json
Introduce TLSClientCertConfiguration and SubgraphTLSConfiguration; extend TLSConfiguration with Subgraph and add schema entries for global and per‑subgraph TLS client cert/CA settings with dependency rules.
Config fixtures & defaults
router/pkg/config/fixtures/full.yaml, router/pkg/config/testdata/config_defaults.json, router/pkg/config/testdata/config_full.json
Add example and test default/full JSON/YAML entries demonstrating tls.subgraph global and per‑subgraph settings.
TLS assembly logic
router/core/tls.go, router/core/tls_test.go
Add buildTLSClientConfig and buildSubgraphTLSConfigs to build *tls.Config for global and per‑subgraph settings; include unit tests covering success and error cases for cert/CA loading and InsecureSkipVerify.
Router public API & config storage
router/core/router.go, router/core/router_config.go, router/core/supervisor_instance.go
Add WithSubgraphTLSConfiguration option and store subgraphTLSConfiguration in router Config; pass client TLS into transport creation.
Transport & resolver plumbing
router/core/graph_server.go, router/core/factoryresolver.go
Build default and per‑subgraph TLS configs, propagate them into HTTP transports, and add a secondary pass to create transports for subgraphs that have TLS configs but lacked transport entries.
Test environment TLS support
router-tests/testenv/testenv.go
Add TLSConfig *tls.Config to SubgraphConfig and helpers makeSafeTLSHttpTestServer / makeSubgraphTestServer to start TLS-enabled httptest servers when TLS is configured.
Integration tests for subgraph mTLS
router-tests/subgraph_mtls_test.go
Add TestSubgraphMTLS suite with subtests exercising InsecureSkipVerify, client cert presentation, missing/incorrect cert failures, and per‑subgraph override behavior, using testenv and GraphQL requests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (46 files):

⚔️ cli/CHANGELOG.md (content)
⚔️ cli/package.json (content)
⚔️ connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go (content)
⚔️ connect/CHANGELOG.md (content)
⚔️ connect/package.json (content)
⚔️ connect/src/wg/cosmo/platform/v1/platform_pb.ts (content)
⚔️ controlplane/CHANGELOG.md (content)
⚔️ controlplane/migrations/meta/_journal.json (content)
⚔️ controlplane/package.json (content)
⚔️ controlplane/src/core/bufservices/api-key/createAPIKey.ts (content)
⚔️ controlplane/src/core/bufservices/api-key/getAPIKeys.ts (content)
⚔️ controlplane/src/core/bufservices/proposal/updateProposal.ts (content)
⚔️ controlplane/src/core/constants.ts (content)
⚔️ controlplane/src/core/repositories/ApiKeyRepository.ts (content)
⚔️ controlplane/src/core/test-util.ts (content)
⚔️ controlplane/src/db/schema.ts (content)
⚔️ controlplane/src/types/index.ts (content)
⚔️ controlplane/test/delete-user.test.ts (content)
⚔️ demo/go.mod (content)
⚔️ demo/go.sum (content)
⚔️ proto/wg/cosmo/platform/v1/platform.proto (content)
⚔️ protographic/CHANGELOG.md (content)
⚔️ protographic/package.json (content)
⚔️ router-tests/go.mod (content)
⚔️ router-tests/go.sum (content)
⚔️ router-tests/testenv/testenv.go (content)
⚔️ router/CHANGELOG.md (content)
⚔️ router/core/factoryresolver.go (content)
⚔️ router/core/graph_server.go (content)
⚔️ router/core/plan_generator.go (content)
⚔️ router/core/router.go (content)
⚔️ router/core/router_config.go (content)
⚔️ router/core/supervisor_instance.go (content)
⚔️ router/go.mod (content)
⚔️ router/go.sum (content)
⚔️ router/package.json (content)
⚔️ router/pkg/config/config.go (content)
⚔️ router/pkg/config/config.schema.json (content)
⚔️ router/pkg/config/fixtures/full.yaml (content)
⚔️ router/pkg/config/testdata/config_defaults.json (content)
⚔️ router/pkg/config/testdata/config_full.json (content)
⚔️ shared/CHANGELOG.md (content)
⚔️ shared/package.json (content)
⚔️ studio/CHANGELOG.md (content)
⚔️ studio/package.json (content)
⚔️ studio/src/pages/[organizationSlug]/apikeys.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add mTLS support' directly and clearly summarizes the main change across all modified files, which collectively implement mutual TLS certificate support for subgraph connections.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

@SkArchon SkArchon marked this pull request as ready for review February 16, 2026 18:26
@SkArchon SkArchon marked this pull request as draft February 16, 2026 18:26
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 16, 2026

Router-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-ac487c290af1c9ddc2563b182fbb40aa83872e62-nonroot

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 92.53731% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.77%. Comparing base (88e4878) to head (1155f14).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
router/core/graph_server.go 86.66% 1 Missing and 1 partial ⚠️
router/core/tls.go 94.87% 1 Missing and 1 partial ⚠️
router/core/supervisor_instance.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2514      +/-   ##
==========================================
+ Coverage   62.21%   62.77%   +0.56%     
==========================================
  Files         241      242       +1     
  Lines       25499    25560      +61     
==========================================
+ Hits        15864    16046     +182     
+ Misses       8298     8178     -120     
+ Partials     1337     1336       -1     
Files with missing lines Coverage Δ
router/core/factoryresolver.go 79.07% <100.00%> (+0.26%) ⬆️
router/core/router.go 69.61% <100.00%> (+0.11%) ⬆️
router/core/router_config.go 93.75% <ø> (ø)
router/pkg/config/config.go 80.51% <ø> (ø)
router/core/supervisor_instance.go 0.00% <0.00%> (ø)
router/core/graph_server.go 84.19% <86.66%> (+1.29%) ⬆️
router/core/tls.go 94.87% <94.87%> (ø)

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 2

🤖 Fix all issues with AI agents
In `@router/core/router.go`:
- Around line 204-206: The tls.Config created in router/core/router.go
(tlsConfig) lacks a MinVersion, allowing downgrades; set tlsConfig.MinVersion =
tls.VersionTLS13 (or tls.VersionTLS12 if older subgraphs require it) when
constructing tlsConfig (the block that uses
clientCfg.InsecureSkipCaVerification) so outbound subgraph client connections
enforce a minimum TLS version.

In `@router/pkg/config/config.schema.json`:
- Around line 455-469: The schema's global TLS client cert block uses
"certificate_authority" but runtime expects "ca_file"; update the TLS global
object (tls.subgraph.all / the TLSClientCertConfiguration schema) to replace the
"certificate_authority" property name with "ca_file" (keeping type "string",
format "file-path" and the description about verifying subgraph server
certificates and falling back to system root CAs) so the schema field matches
the runtime config name and the global CA will be applied correctly.
🧹 Nitpick comments (1)
router/core/router_test.go (1)

424-455: Close test cert files and surface Close errors.

The helper writes PEM files but ignores Close errors. If a Close fails, tests may read a partial file or leak descriptors. Consider deferring Close and asserting errors.

💡 Proposed fix
 	certPath = filepath.Join(dir, prefix+".crt")
 	certFile, err := os.Create(certPath)
 	require.NoError(t, err)
+	defer func() { assert.NoError(t, certFile.Close()) }()
 	require.NoError(t, pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
-	certFile.Close()
 
 	keyPath = filepath.Join(dir, prefix+".key")
 	keyFile, err := os.Create(keyPath)
 	require.NoError(t, err)
+	defer func() { assert.NoError(t, keyFile.Close()) }()
 	keyDER, err := x509.MarshalECPrivateKey(key)
 	require.NoError(t, err)
 	require.NoError(t, pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))
-	keyFile.Close()

Comment thread router/core/router.go Outdated
Comment thread router/pkg/config/config.schema.json Outdated
@SkArchon SkArchon changed the title feat: add mTLS support feat: add mTLS support [WIP] Feb 16, 2026
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.

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@router/core/tls_test.go`:
- Around line 73-83: Add a new unit test in tls_test.go that exercises
buildTLSClientConfig with a partial TLSClientCertConfiguration: call
generateTestCert(t, "client") to get certPath and keyPath, then invoke
buildTLSClientConfig twice—once with only CertificateChain set (no Key) and once
with only Key set (no CertificateChain)—and assert that it returns an error or a
nil/empty tls.Config.Certificates as the implementation should handle incomplete
configs; reference the existing test "loads client cert and key", function
buildTLSClientConfig, and type config.TLSClientCertConfiguration when adding
these assertions.

In `@router/core/tls.go`:
- Around line 18-25: The code silently skips loading client cert when
clientCfg.CertificateChain and clientCfg.Key are not both set; update the block
that currently calls tls.LoadX509KeyPair to check for the asymmetric case (one
is set but the other is empty) and emit a clear warning (or return an error if
preferred) referencing clientCfg.CertificateChain and clientCfg.Key so users
know the cert/key pair is incomplete; keep the existing tls.LoadX509KeyPair and
setting of tlsConfig.Certificates when both are present and only add the
conditional warning path before attempting to load.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@router/core/tls_test.go`:
- Around line 73-83: Add a new unit test in tls_test.go that exercises
buildTLSClientConfig with a partial TLSClientCertConfiguration: call
generateTestCert(t, "client") to get certPath and keyPath, then invoke
buildTLSClientConfig twice—once with only CertificateChain set (no Key) and once
with only Key set (no CertificateChain)—and assert that it returns an error or a
nil/empty tls.Config.Certificates as the implementation should handle incomplete
configs; reference the existing test "loads client cert and key", function
buildTLSClientConfig, and type config.TLSClientCertConfiguration when adding
these assertions.

In `@router/core/tls.go`:
- Around line 18-25: The code silently skips loading client cert when
clientCfg.CertificateChain and clientCfg.Key are not both set; update the block
that currently calls tls.LoadX509KeyPair to check for the asymmetric case (one
is set but the other is empty) and emit a clear warning (or return an error if
preferred) referencing clientCfg.CertificateChain and clientCfg.Key so users
know the cert/key pair is incomplete; keep the existing tls.LoadX509KeyPair and
setting of tlsConfig.Certificates when both are present and only add the
conditional warning path before attempting to load.
router/core/tls_test.go (1)

73-83: Consider adding a test for partial certificate configuration.

The current tests don't cover the case where only CertificateChain is provided without Key (or vice versa). This would help ensure the implementation handles incomplete configurations gracefully.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@router/core/tls_test.go` around lines 73 - 83, Add a new unit test in
tls_test.go that exercises buildTLSClientConfig with a partial
TLSClientCertConfiguration: call generateTestCert(t, "client") to get certPath
and keyPath, then invoke buildTLSClientConfig twice—once with only
CertificateChain set (no Key) and once with only Key set (no
CertificateChain)—and assert that it returns an error or a nil/empty
tls.Config.Certificates as the implementation should handle incomplete configs;
reference the existing test "loads client cert and key", function
buildTLSClientConfig, and type config.TLSClientCertConfiguration when adding
these assertions.
router/core/tls.go (1)

18-25: Consider warning when certificate configuration is incomplete.

If a user provides CertificateChain without Key (or vice versa), the configuration is silently ignored. This could lead to confusion during troubleshooting.

🛡️ Suggested enhancement
 	// Load client certificate and key if provided
-	if clientCfg.CertificateChain != "" && clientCfg.Key != "" {
+	hasCert := clientCfg.CertificateChain != ""
+	hasKey := clientCfg.Key != ""
+	if hasCert != hasKey {
+		return nil, fmt.Errorf("both certificate_chain and key must be provided together")
+	}
+	if hasCert && hasKey {
 		cert, err := tls.LoadX509KeyPair(clientCfg.CertificateChain, clientCfg.Key)
 		if err != nil {
 			return nil, fmt.Errorf("failed to load client TLS cert and key: %w", err)
 		}
 		tlsConfig.Certificates = []tls.Certificate{cert}
 	}
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@router/core/tls.go` around lines 18 - 25, The code silently skips loading
client cert when clientCfg.CertificateChain and clientCfg.Key are not both set;
update the block that currently calls tls.LoadX509KeyPair to check for the
asymmetric case (one is set but the other is empty) and emit a clear warning (or
return an error if preferred) referencing clientCfg.CertificateChain and
clientCfg.Key so users know the cert/key pair is incomplete; keep the existing
tls.LoadX509KeyPair and setting of tlsConfig.Certificates when both are present
and only add the conditional warning path before attempting to load.

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.

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@router-tests/subgraph_mtls_test.go`:
- Around line 31-40: The test TLS config created in subgraphMTLSServerConfig
does not set MinVersion; update the function to set cfg.MinVersion =
tls.VersionTLS12 (or tls.VersionTLS13) to enforce a modern TLS minimum for the
httptest server, keeping existing behavior for client cert handling and still
using loadSubgraphMTLSCACertPool when requireClientCert is true.
🧹 Nitpick comments (1)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@router-tests/subgraph_mtls_test.go`:
- Around line 31-40: The test TLS config created in subgraphMTLSServerConfig
does not set MinVersion; update the function to set cfg.MinVersion =
tls.VersionTLS12 (or tls.VersionTLS13) to enforce a modern TLS minimum for the
httptest server, keeping existing behavior for client cert handling and still
using loadSubgraphMTLSCACertPool when requireClientCert is true.
router-tests/subgraph_mtls_test.go (1)

31-40: Consider setting MinVersion on test TLS config.

Static analysis flags that MinVersion is not set on the tls.Config. While this is test code with low security risk (local httptest server), setting MinVersion: tls.VersionTLS12 or tls.VersionTLS13 is a good practice for consistency and to serve as example code.

🔧 Proposed fix
 func subgraphMTLSServerConfig(t *testing.T, requireClientCert bool) *tls.Config {
 	t.Helper()
-	cfg := &tls.Config{}
+	cfg := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
 	if requireClientCert {
 		caPool := loadSubgraphMTLSCACertPool(t, "testdata/tls/cert.pem")
 		cfg.ClientCAs = caPool
 		cfg.ClientAuth = tls.RequireAndVerifyClientCert
 	}
 	return cfg
 }
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@router-tests/subgraph_mtls_test.go` around lines 31 - 40, The test TLS config
created in subgraphMTLSServerConfig does not set MinVersion; update the function
to set cfg.MinVersion = tls.VersionTLS12 (or tls.VersionTLS13) to enforce a
modern TLS minimum for the httptest server, keeping existing behavior for client
cert handling and still using loadSubgraphMTLSCACertPool when requireClientCert
is true.

@SkArchon SkArchon changed the title feat: add mTLS support [WIP] feat: add mTLS support Feb 17, 2026
@SkArchon SkArchon marked this pull request as ready for review February 17, 2026 21:26
Copy link
Copy Markdown
Contributor

@StarpTech StarpTech left a comment

Choose a reason for hiding this comment

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

It should also be possible to use a shared certificate for all subgraphs, with the option to override it for individual subgraphs.

tls:
  client:
    all:
       cert_file: "/Users/milindadias/Work/cosmo/router/certs/employee.crt"
       key_file: "/Users/milindadias/Work/cosmo/router/certs/employee.key"
    subgraphs:
      employee:
        cert_file: "/Users/milindadias/Work/cosmo/router/certs/employee.crt"
        key_file: "/Users/milindadias/Work/cosmo/router/certs/employee.key"

Comment thread router/pkg/config/config.go Outdated
Comment thread router/pkg/config/config.go Outdated
Copy link
Copy Markdown
Contributor

@StarpTech StarpTech left a comment

Choose a reason for hiding this comment

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

LGTM

@SkArchon SkArchon merged commit 153432d into main Feb 24, 2026
41 of 42 checks passed
@SkArchon SkArchon deleted the milinda/mTLS branch February 24, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants