Skip to content

Commit f5441e6

Browse files
authored
Merge pull request #7023 from hashicorp/b-tls-validation
Validate role and region for mTLS
2 parents dd8fbf4 + cb9274a commit f5441e6

File tree

3 files changed

+109
-13
lines changed

3 files changed

+109
-13
lines changed

nomad/server.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package nomad
33
import (
44
"context"
55
"crypto/tls"
6+
"crypto/x509"
7+
"errors"
68
"fmt"
79
"io/ioutil"
810
"net"
@@ -11,6 +13,7 @@ import (
1113
"path/filepath"
1214
"sort"
1315
"strconv"
16+
"strings"
1417
"sync"
1518
"sync/atomic"
1619
"time"
@@ -279,7 +282,7 @@ func NewServer(config *Config, consulCatalog consul.CatalogAPI) (*Server, error)
279282
if err != nil {
280283
return nil, err
281284
}
282-
incomingTLS, tlsWrap, err := getTLSConf(config.TLSConfig.EnableRPC, tlsConf)
285+
incomingTLS, tlsWrap, err := getTLSConf(config.TLSConfig.EnableRPC, tlsConf, config.Region)
283286
if err != nil {
284287
return nil, err
285288
}
@@ -447,25 +450,70 @@ func (s *Server) createRPCListener() (*net.TCPListener, error) {
447450

448451
// getTLSConf gets the server's TLS configuration based on the config supplied
449452
// by the operator
450-
func getTLSConf(enableRPC bool, tlsConf *tlsutil.Config) (*tls.Config, tlsutil.RegionWrapper, error) {
453+
func getTLSConf(enableRPC bool, tlsConf *tlsutil.Config, region string) (*tls.Config, tlsutil.RegionWrapper, error) {
451454
var tlsWrap tlsutil.RegionWrapper
452455
var incomingTLS *tls.Config
453-
if enableRPC {
454-
tw, err := tlsConf.OutgoingTLSWrapper()
455-
if err != nil {
456-
return nil, nil, err
457-
}
458-
tlsWrap = tw
456+
if !enableRPC {
457+
return incomingTLS, tlsWrap, nil
458+
}
459459

460-
itls, err := tlsConf.IncomingTLSConfig()
461-
if err != nil {
462-
return nil, nil, err
463-
}
460+
tlsWrap, err := tlsConf.OutgoingTLSWrapper()
461+
if err != nil {
462+
return nil, nil, err
463+
}
464+
465+
itls, err := tlsConf.IncomingTLSConfig()
466+
if err != nil {
467+
return nil, nil, err
468+
}
469+
470+
if tlsConf.VerifyServerHostname {
471+
incomingTLS = itls.Clone()
472+
incomingTLS.VerifyPeerCertificate = rpcNameAndRegionValidator(region)
473+
} else {
464474
incomingTLS = itls
465475
}
466476
return incomingTLS, tlsWrap, nil
467477
}
468478

479+
// implements signature of tls.Config.VerifyPeerCertificate which is called
480+
// after the certs have been verified. We'll ignore the raw certs and only
481+
// check the verified certs.
482+
func rpcNameAndRegionValidator(region string) func([][]byte, [][]*x509.Certificate) error {
483+
return func(_ [][]byte, certificates [][]*x509.Certificate) error {
484+
if len(certificates) > 0 && len(certificates[0]) > 0 {
485+
cert := certificates[0][0]
486+
for _, dnsName := range cert.DNSNames {
487+
if validateRPCRegionPeer(dnsName, region) {
488+
return nil
489+
}
490+
}
491+
if validateRPCRegionPeer(cert.Subject.CommonName, region) {
492+
return nil
493+
}
494+
}
495+
return errors.New("invalid role or region for certificate")
496+
}
497+
}
498+
499+
func validateRPCRegionPeer(name, region string) bool {
500+
parts := strings.Split(name, ".")
501+
if len(parts) < 3 {
502+
// Invalid SAN
503+
return false
504+
}
505+
if parts[len(parts)-1] != "nomad" {
506+
// Incorrect service
507+
return false
508+
}
509+
if parts[0] == "client" {
510+
// Clients may only connect to servers in their region
511+
return name == "client."+region+".nomad"
512+
}
513+
// Servers may connect to any Nomad RPC service for federation.
514+
return parts[0] == "server"
515+
}
516+
469517
// reloadTLSConnections updates a server's TLS configuration and reloads RPC
470518
// connections.
471519
func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error {
@@ -483,7 +531,7 @@ func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error {
483531
return err
484532
}
485533

486-
incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf)
534+
incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf, s.config.Region)
487535
if err != nil {
488536
s.logger.Error("unable to reset TLS context", "error", err)
489537
return err

nomad/server_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,30 @@ func TestServer_InvalidSchedulers(t *testing.T) {
558558
require.NotNil(err)
559559
require.Contains(err.Error(), "foo")
560560
}
561+
562+
func TestServer_RPCNameAndRegionValidation(t *testing.T) {
563+
t.Parallel()
564+
for _, tc := range []struct {
565+
name string
566+
region string
567+
expected bool
568+
}{
569+
// OK
570+
{name: "client.global.nomad", region: "global", expected: true},
571+
{name: "server.global.nomad", region: "global", expected: true},
572+
{name: "server.other.nomad", region: "global", expected: true},
573+
{name: "server.other.region.nomad", region: "other.region", expected: true},
574+
575+
// Bad
576+
{name: "client.other.nomad", region: "global", expected: false},
577+
{name: "client.global.nomad.other", region: "global", expected: false},
578+
{name: "server.global.nomad.other", region: "global", expected: false},
579+
{name: "other.global.nomad", region: "global", expected: false},
580+
{name: "server.nomad", region: "global", expected: false},
581+
{name: "localhost", region: "global", expected: false},
582+
} {
583+
assert.Equal(t, tc.expected, validateRPCRegionPeer(tc.name, tc.region),
584+
"expected %q in region %q to validate as %v",
585+
tc.name, tc.region, tc.expected)
586+
}
587+
}

website/source/guides/upgrade/upgrade-specific.html.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ standard upgrade flow.
1717

1818
## Nomad 0.10.3
1919

20+
### mTLS Certificate Validation
21+
22+
Nomad 0.10.3 includes a fix for a privilege escalation vulnerability in
23+
validating TLS certificates for RPC with mTLS. Nomad RPC endpoints validated
24+
that TLS client certificates had not expired and were signed by the same CA as
25+
the Nomad node, but did not correctly check the certificate's name for the role
26+
and region as described in the [Securing Nomad with TLS][tls-guide] guide. This
27+
allows trusted operators with a client certificate signed by the CA to send RPC
28+
calls as a Nomad client or server node, bypassing access control and accessing
29+
any secrets available to a client.
30+
31+
Nomad clusters configured for mTLS following the [Securing Nomad with TLS][tls-guide]
32+
guide or the [Vault PKI Secrets Engine Integration][tls-vault-guide] guide
33+
should already have certificates that will pass validation. Before upgrading to
34+
Nomad 0.10.3, operators using mTLS with `verify_server_hostname = true` should
35+
confirm that the common name or SAN of all Nomad client node certs is
36+
`client.<region>.nomad`, and that the common name or SAN of all Nomad server
37+
node certs is `server.<region>.nomad`.
38+
2039
### Connection Limits Added
2140

2241
Nomad 0.10.3 introduces the [limits][limits] agent configuration parameters for
@@ -419,3 +438,5 @@ deleted and then Nomad 0.3.0 can be launched.
419438
[task-config]: /docs/job-specification/task.html#config
420439
[validate]: /docs/commands/job/validate.html
421440
[update]: /docs/job-specification/update.html
441+
[tls-guide]: /guides/security/securing-nomad.html
442+
[tls-vault-guide]: /guides/security/vault-pki-integration.html

0 commit comments

Comments
 (0)