Skip to content

Commit b9b8428

Browse files
Allow only CA cert to be set in command server TLS settings (#1116)
* test: use smaller selfsigned certs for testing When generating a self-signed cert for _unit-tests_ it doesn't need to have high security. Using a smaller length makes the tests run faster. The difference is ~0.1s vs ~2.0s to run one test that generates a cert. * test: fix grpc mTLS dialoptions test This was incorrectly passing in the cert and key backwards which resulted in not actually getting mTLS credentials, but instead insecure credentials. When insecure credentials are used, skipToken means we don't add a addPerRPCCredentials. When it is a secure TLS credential then addPerRPCCredentials increases the dialoption count by one. * fix: Use specified CA cert for grpc This had been skipping out of the function early if a client key wasn't specified. I don't believe that's correct. If I[User] have specified specified a CA cert because the MPI server I'm trying to talk to is signed by a non-standard CA (e.g. N1 devenv) then it should be respected regardless of whether I've configured mTLS. Silently skipping the CA is really confusing and leads to > Failed to create connection" error="rpc error: code = Unavailable desc = connection error: desc = \"transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority\" I've split out the getTLSConfigForCredentials to make it easier to test this translation. Once it is wrapped into a TransportCredential or a DialOption it's opaque and hard to verify. * fix: invalid TLS CA cert should error immediately Previously if a consumer specified the CA cert to verify the command connection but that CA wasn't valid then system would log at Debug (default hidden) and proceed anyways. I don't believe this is good behavior. If the consumer is directly specifying a CA cert then that is the CA that should be used, not silently ignored. This patch returns the error up, which is now caught and swallowed at a higher level, but at least it is more visible: > time=2025-05-21T15:41:33.547Z level=ERROR msg="Unable to add transport credentials to gRPC dial options, adding default transport credentials" error="invalid CA cert while building transport credentials: read CA file (/etc/nginx-agent/bad.crt): open /etc/nginx-agent/bad.crt: no such file or directory" --------- Co-authored-by: Nathan Bird <n.bird@f5.com>
1 parent c89d9f1 commit b9b8428

File tree

3 files changed

+148
-43
lines changed

3 files changed

+148
-43
lines changed

internal/grpc/grpc.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -363,30 +363,32 @@ func getTransportCredentials(agentConfig *config.Config) (credentials.TransportC
363363
if agentConfig.Command.TLS == nil {
364364
return defaultCredentials, nil
365365
}
366+
tlsConfig, err := getTLSConfigForCredentials(agentConfig.Command.TLS)
367+
if err != nil {
368+
return nil, err
369+
}
370+
371+
return credentials.NewTLS(tlsConfig), nil
372+
}
366373

367-
if agentConfig.Command.TLS.SkipVerify {
374+
func getTLSConfigForCredentials(c *config.TLSConfig) (*tls.Config, error) {
375+
if c.SkipVerify {
368376
slog.Warn("Verification of the server's certificate chain and host name is disabled")
369377
}
370378

371379
tlsConfig := &tls.Config{
372380
MinVersion: tls.VersionTLS12,
373-
ServerName: agentConfig.Command.TLS.ServerName,
374-
InsecureSkipVerify: agentConfig.Command.TLS.SkipVerify,
381+
ServerName: c.ServerName,
382+
InsecureSkipVerify: c.SkipVerify,
375383
}
376384

377-
if agentConfig.Command.TLS.Key == "" {
378-
return credentials.NewTLS(tlsConfig), nil
385+
if err := appendRootCAs(tlsConfig, c.Ca); err != nil {
386+
return nil, fmt.Errorf("invalid CA cert while building transport credentials: %w", err)
379387
}
380388

381-
err := appendCertKeyPair(tlsConfig, agentConfig.Command.TLS.Cert, agentConfig.Command.TLS.Key)
382-
if err != nil {
383-
return nil, fmt.Errorf("append cert and key pair failed: %w", err)
389+
if err := appendCertKeyPair(tlsConfig, c.Cert, c.Key); err != nil {
390+
return nil, fmt.Errorf("invalid client cert while building transport credentials: %w", err)
384391
}
385392

386-
err = appendRootCAs(tlsConfig, agentConfig.Command.TLS.Ca)
387-
if err != nil {
388-
slog.Debug("Unable to append root CA", "error", err)
389-
}
390-
391-
return credentials.NewTLS(tlsConfig), nil
393+
return tlsConfig, nil
392394
}

internal/grpc/grpc_test.go

Lines changed: 129 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@ package grpc
77

88
import (
99
"context"
10+
"crypto/tls"
11+
"crypto/x509"
1012
"fmt"
1113
"testing"
1214

13-
"google.golang.org/grpc/credentials"
14-
1515
"github.com/cenkalti/backoff/v4"
16-
"github.com/nginx/agent/v3/test/helpers"
17-
"github.com/nginx/agent/v3/test/protos"
1816
"google.golang.org/grpc"
1917
"google.golang.org/grpc/codes"
2018
"google.golang.org/grpc/metadata"
2119
"google.golang.org/grpc/status"
2220

21+
"github.com/nginx/agent/v3/test/helpers"
22+
"github.com/nginx/agent/v3/test/protos"
23+
2324
"github.com/nginx/agent/v3/internal/config"
2425
"github.com/nginx/agent/v3/test/types"
2526

@@ -103,7 +104,7 @@ func Test_GetDialOptions(t *testing.T) {
103104
{
104105
name: "Test 2: DialOptions mTLS",
105106
agentConfig: types.AgentConfig(),
106-
expected: 7,
107+
expected: 8,
107108
createCerts: true,
108109
},
109110
{
@@ -171,8 +172,8 @@ func Test_GetDialOptions(t *testing.T) {
171172
key, cert := helpers.GenerateSelfSignedCert(t)
172173
_, ca := helpers.GenerateSelfSignedCert(t)
173174

174-
keyContents := helpers.Cert{Name: keyFileName, Type: certificateType, Contents: key}
175-
certContents := helpers.Cert{Name: certFileName, Type: privateKeyType, Contents: cert}
175+
keyContents := helpers.Cert{Name: keyFileName, Type: privateKeyType, Contents: key}
176+
certContents := helpers.Cert{Name: certFileName, Type: certificateType, Contents: cert}
176177
caContents := helpers.Cert{Name: caFileName, Type: certificateType, Contents: ca}
177178

178179
helpers.WriteCertFiles(t, tmpDir, keyContents)
@@ -356,28 +357,136 @@ func Test_ValidateGrpcError(t *testing.T) {
356357
}
357358

358359
func Test_getTransportCredentials(t *testing.T) {
359-
tests := []struct {
360-
want credentials.TransportCredentials
361-
conf *config.Config
362-
wantErr assert.ErrorAssertionFunc
363-
name string
360+
tests := map[string]struct {
361+
conf *config.Config
362+
wantSecurityProfile string
363+
wantServerName string
364+
wantErr bool
364365
}{
365-
{
366-
name: "No TLS config returns default credentials",
366+
"Test 1: No TLS config returns default credentials": {
367367
conf: &config.Config{
368368
Command: &config.Command{},
369369
},
370-
want: defaultCredentials,
371-
wantErr: assert.NoError,
370+
wantErr: false,
371+
wantSecurityProfile: "insecure",
372+
},
373+
"Test 2: With tls config returns secure credentials": {
374+
conf: &config.Config{
375+
Command: &config.Command{
376+
TLS: &config.TLSConfig{
377+
ServerName: "foobar",
378+
SkipVerify: true,
379+
},
380+
},
381+
},
382+
wantErr: false,
383+
wantSecurityProfile: "tls",
384+
},
385+
"Test 3: With invalid tls config should error": {
386+
conf: types.AgentConfig(), // references non-existent certs
387+
wantErr: true,
372388
},
373389
}
374-
for _, tt := range tests {
375-
t.Run(tt.name, func(t *testing.T) {
390+
for name, tt := range tests {
391+
t.Run(name, func(t *testing.T) {
376392
got, err := getTransportCredentials(tt.conf)
377-
if !tt.wantErr(t, err, fmt.Sprintf("getTransportCredentials(%v)", tt.conf)) {
393+
if tt.wantErr {
394+
require.Error(t, err, "getTransportCredentials(%v)", tt.conf)
395+
378396
return
379397
}
380-
assert.Equalf(t, tt.want, got, "getTransportCredentials(%v)", tt.conf)
398+
require.NoError(t, err, "getTransportCredentials(%v)", tt.conf)
399+
require.Equal(t, tt.wantSecurityProfile, got.Info().SecurityProtocol, "incorrect SecurityProtocol")
400+
})
401+
}
402+
}
403+
404+
func Test_getTLSConfig(t *testing.T) {
405+
tmpDir := t.TempDir()
406+
// not mTLS scripts
407+
key, cert := helpers.GenerateSelfSignedCert(t)
408+
_, ca := helpers.GenerateSelfSignedCert(t)
409+
410+
keyContents := helpers.Cert{Name: keyFileName, Type: privateKeyType, Contents: key}
411+
certContents := helpers.Cert{Name: certFileName, Type: certificateType, Contents: cert}
412+
caContents := helpers.Cert{Name: caFileName, Type: certificateType, Contents: ca}
413+
414+
keyPath := helpers.WriteCertFiles(t, tmpDir, keyContents)
415+
certPath := helpers.WriteCertFiles(t, tmpDir, certContents)
416+
caPath := helpers.WriteCertFiles(t, tmpDir, caContents)
417+
418+
tests := map[string]struct {
419+
conf *config.TLSConfig
420+
verify func(require.TestingT, *tls.Config)
421+
wantErr bool
422+
}{
423+
"Test 1: all config should be translated": {
424+
conf: &config.TLSConfig{
425+
Cert: certPath,
426+
Key: keyPath,
427+
Ca: caPath,
428+
ServerName: "foobar",
429+
SkipVerify: true,
430+
},
431+
wantErr: false,
432+
verify: func(t require.TestingT, c *tls.Config) {
433+
require.NotEmpty(t, c.Certificates)
434+
require.Equal(t, "foobar", c.ServerName, "wrong servername")
435+
require.True(t, c.InsecureSkipVerify, "InsecureSkipVerify not set")
436+
},
437+
},
438+
"Test 2: CA only config should use CA": {
439+
conf: &config.TLSConfig{
440+
Ca: caPath,
441+
},
442+
wantErr: false,
443+
verify: func(t require.TestingT, c *tls.Config) {
444+
require.NotNil(t, c.RootCAs, "RootCAs should be initialized")
445+
require.False(t, x509.NewCertPool().Equal(c.RootCAs),
446+
"CertPool shouldn't be empty, valid CA cert was specified")
447+
require.False(t, c.InsecureSkipVerify, "InsecureSkipVerify should not be set")
448+
},
449+
},
450+
"Test 3: incorrect CA should not error": {
451+
conf: &config.TLSConfig{
452+
Ca: "customca.pem",
453+
},
454+
wantErr: true,
455+
},
456+
"Test 4: incorrect key path should error": {
457+
conf: &config.TLSConfig{
458+
Ca: caPath,
459+
Cert: certPath,
460+
Key: "badkey",
461+
},
462+
wantErr: true,
463+
},
464+
"Test 5: incorrect cert path should error": {
465+
conf: &config.TLSConfig{
466+
Ca: caPath,
467+
Cert: "badcert",
468+
Key: keyPath,
469+
},
470+
wantErr: true,
471+
},
472+
"Test 6: incomplete cert info should error": {
473+
conf: &config.TLSConfig{
474+
Key: keyPath,
475+
},
476+
wantErr: true,
477+
},
478+
}
479+
for name, tt := range tests {
480+
t.Run(name, func(t *testing.T) {
481+
got, err := getTLSConfigForCredentials(tt.conf)
482+
if tt.wantErr {
483+
require.Error(t, err, "getTLSConfigForCredentials(%v)", tt.conf)
484+
return
485+
}
486+
require.NoError(t, err, "getTLSConfigForCredentials(%v)", tt.conf)
487+
if tt.verify != nil {
488+
tt.verify(t, got)
489+
}
381490
})
382491
}
383492
}

test/helpers/cert_utils.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import (
1111
"crypto/x509"
1212
"crypto/x509/pkix"
1313
"encoding/pem"
14-
"fmt"
1514
"math/big"
1615
"os"
17-
"strings"
16+
"path"
1817
"testing"
1918
"time"
2019

@@ -31,7 +30,7 @@ const (
3130
permission = 0o600
3231
serialNumber = 123123
3332
years, months, days = 5, 0, 0
34-
bits = 4096
33+
bits = 1024
3534
)
3635

3736
func GenerateSelfSignedCert(t testing.TB) (keyBytes, certBytes []byte) {
@@ -73,12 +72,7 @@ func WriteCertFiles(t *testing.T, location string, cert Cert) string {
7372
Bytes: cert.Contents,
7473
})
7574

76-
var certFile string
77-
if strings.HasSuffix(location, string(os.PathSeparator)) {
78-
certFile = fmt.Sprintf("%s%s", location, cert.Name)
79-
} else {
80-
certFile = fmt.Sprintf("%s%s%s", location, string(os.PathSeparator), cert.Name)
81-
}
75+
certFile := path.Join(location, cert.Name)
8276

8377
err := os.WriteFile(certFile, pemContents, permission)
8478
require.NoError(t, err)

0 commit comments

Comments
 (0)