From 595be332c64922f3ca4490956d1226b507eeac44 Mon Sep 17 00:00:00 2001 From: Aaron Blum Date: Wed, 17 Feb 2021 16:32:58 -0500 Subject: [PATCH] server: added initial cert utilities for automatic certificate generation server: added utility function for bundling init certs server: added init function that uses a recieved bundle to provision a node security: added helper functions to support automatic certificate generation security: added ClientCAKeyPath helper to align with ClientCACertPath This is part of #60632 and provides functions for #60636. Release note: None --- pkg/security/BUILD.bazel | 2 + pkg/security/auto_tls_init.go | 227 +++++++++++++ pkg/security/auto_tls_init_test.go | 50 +++ pkg/security/certificate_manager.go | 112 +++++++ pkg/server/BUILD.bazel | 3 + pkg/server/auto_tls_init.go | 476 ++++++++++++++++++++++++++++ pkg/server/auto_tls_init_test.go | 91 ++++++ 7 files changed, 961 insertions(+) create mode 100644 pkg/security/auto_tls_init.go create mode 100644 pkg/security/auto_tls_init_test.go create mode 100644 pkg/server/auto_tls_init.go create mode 100644 pkg/server/auto_tls_init_test.go diff --git a/pkg/security/BUILD.bazel b/pkg/security/BUILD.bazel index cca9bcef3f29..e65540200470 100644 --- a/pkg/security/BUILD.bazel +++ b/pkg/security/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "security", srcs = [ "auth.go", + "auto_tls_init.go", "certificate_loader.go", "certificate_manager.go", "certs.go", @@ -47,6 +48,7 @@ go_test( size = "medium", srcs = [ "auth_test.go", + "auto_tls_init_test.go", "certificate_loader_test.go", "certificate_manager_test.go", "certs_rotation_test.go", diff --git a/pkg/security/auto_tls_init.go b/pkg/security/auto_tls_init.go new file mode 100644 index 000000000000..cf53c6c9f95e --- /dev/null +++ b/pkg/security/auto_tls_init.go @@ -0,0 +1,227 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package security + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "time" + + "github.com/cockroachdb/cockroach/pkg/util/timeutil" + "github.com/cockroachdb/errors" +) + +// TODO(aaron-crl): This shared a name and purpose with the value in +// pkg/security and should be consolidated. +const defaultKeySize = 4096 + +// notBeforeMargin provides a window to compensate for potential clock skew. +const notBeforeMargin = time.Second * 30 + +// createCertificateSerialNumber is a helper function that generates a +// random value between [1, 2^130). The use of crypto random for a serial with +// greater than 128 bits of entropy provides for a potential future where we +// decided to rely on the serial for security purposes. +func createCertificateSerialNumber() (serialNumber *big.Int, err error) { + max := new(big.Int) + max.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(max, big.NewInt(1)) + + // serialNumber is set using rand.Int which yields a value between [0, max) + // where max is (2^130)-1. + serialNumber, err = rand.Int(rand.Reader, max) + if err != nil { + err = errors.Wrap(err, "failed to create new serial number") + } + + // We then add 1 to the result ensuring a range of [1,2^130). + serialNumber.Add(serialNumber, big.NewInt(1)) + + return +} + +// CreateCACertAndKey will create a CA with a validity beginning +// now() and expiring after `lifespan`. This is a utility function to help +// with cluster auto certificate generation. +func CreateCACertAndKey( + lifespan time.Duration, service string, +) (certPEM []byte, keyPEM []byte, err error) { + notBefore := timeutil.Now().Add(-notBeforeMargin) + notAfter := timeutil.Now().Add(lifespan) + + // Create random serial number for CA. + serialNumber, err := createCertificateSerialNumber() + if err != nil { + return nil, nil, err + } + + // Create short lived initial CA template. + ca := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Cockroach Labs"}, + OrganizationalUnit: []string{service}, + Country: []string{"US"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: true, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + MaxPathLenZero: true, + } + + // Create private and public key for CA. + caPrivKey, err := rsa.GenerateKey(rand.Reader, defaultKeySize) + if err != nil { + return nil, nil, err + } + + caPrivKeyPEM := new(bytes.Buffer) + caPrivKeyPEMBytes, err := x509.MarshalPKCS8PrivateKey(caPrivKey) + if err != nil { + return nil, nil, err + } + + err = pem.Encode(caPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: caPrivKeyPEMBytes, + }) + if err != nil { + return nil, nil, err + } + + // Create CA certificate then PEM encode it. + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, nil, err + } + + caPEM := new(bytes.Buffer) + err = pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + if err != nil { + return nil, nil, err + } + + certPEM = caPEM.Bytes() + keyPEM = caPrivKeyPEM.Bytes() + + return certPEM, keyPEM, nil +} + +// CreateServiceCertAndKey creates a cert/key pair signed by the provided CA. +// This is a utility function to help with cluster auto certificate generation. +func CreateServiceCertAndKey( + lifespan time.Duration, service, hostname string, caCertPEM []byte, caKeyPEM []byte, +) (certPEM []byte, keyPEM []byte, err error) { + notBefore := timeutil.Now().Add(-notBeforeMargin) + notAfter := timeutil.Now().Add(lifespan) + + // Create random serial number for CA. + serialNumber, err := createCertificateSerialNumber() + if err != nil { + return nil, nil, err + } + + caCertBlock, _ := pem.Decode(caCertPEM) + if caCertBlock == nil { + err = errors.New("failed to parse valid PEM from CaCertificate blob") + return nil, nil, err + } + + caCert, err := x509.ParseCertificate(caCertBlock.Bytes) + if err != nil { + err = errors.Wrap(err, "failed to parse valid Certificate from PEM blob") + return nil, nil, err + } + + caKeyBlock, _ := pem.Decode(caKeyPEM) + if caKeyBlock == nil { + err = errors.New("failed to parse valid PEM from CaKey blob") + return nil, nil, err + } + + caKey, err := x509.ParsePKCS8PrivateKey(caKeyBlock.Bytes) + if err != nil { + err = errors.Wrap(err, "failed to parse valid Certificate from PEM blob") + return nil, nil, err + } + + // Bulid service certificate template; template will be used for all + // autogenerated service certificates. + // TODO(aaron-crl): This should match the implementation in + // pkg/security/x509.go until we can consolidate them. + serviceCert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Cockroach Labs"}, + OrganizationalUnit: []string{service}, + Country: []string{"US"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + } + + // Attempt to parse hostname as IP, if successful add it as an IP + // otherwise presume it is a DNS name. + // TODO(aaron-crl): Pass these values via config object. + ip := net.ParseIP(hostname) + if ip != nil { + serviceCert.IPAddresses = []net.IP{ip} + } else { + serviceCert.DNSNames = []string{hostname} + } + + servicePrivKey, err := rsa.GenerateKey(rand.Reader, defaultKeySize) + if err != nil { + return nil, nil, err + } + + serviceCertBytes, err := x509.CreateCertificate(rand.Reader, serviceCert, caCert, &servicePrivKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + serviceCertBlock := new(bytes.Buffer) + err = pem.Encode(serviceCertBlock, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serviceCertBytes, + }) + if err != nil { + return nil, nil, err + } + + servicePrivKeyPEM := new(bytes.Buffer) + certPrivKeyPEMBytes, err := x509.MarshalPKCS8PrivateKey(servicePrivKey) + if err != nil { + return nil, nil, err + } + + err = pem.Encode(servicePrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: certPrivKeyPEMBytes, + }) + if err != nil { + return nil, nil, err + } + + return serviceCertBlock.Bytes(), servicePrivKeyPEM.Bytes(), err +} diff --git a/pkg/security/auto_tls_init_test.go b/pkg/security/auto_tls_init_test.go new file mode 100644 index 000000000000..9848aeda3a1b --- /dev/null +++ b/pkg/security/auto_tls_init_test.go @@ -0,0 +1,50 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package security_test + +import ( + "testing" + "time" + + "github.com/cockroachdb/cockroach/pkg/security" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" +) + +// TestDummyCreateCACertAndKey is a placeholder for actual testing functions +// TODO(aaron-crl): [tests] write unit tests +func TestDummyCreateCACertAndKey(t *testing.T) { + defer leaktest.AfterTest(t)() + _, _, err := security.CreateCACertAndKey(time.Hour, "test CA cert generation") + if err != nil { + t.Fatalf("expected err=nil, got: %s", err) + } +} + +// TestDummyCreateServiceCertAndKey is a placeholder for actual testing functions +// TODO(aaron-crl): [tests] write unit tests +func TestDummyCreateServiceCertAndKey(t *testing.T) { + defer leaktest.AfterTest(t)() + caCert, caKey, err := security.CreateCACertAndKey(time.Hour, "test CA cert generation") + if err != nil { + t.Fatalf("expected err=nil, got: %s", err) + } + + _, _, err = security.CreateServiceCertAndKey( + time.Minute, + "test Service cert generation", + "localhost", + caCert, + caKey, + ) + if err != nil { + t.Fatalf("expected err=nil, got: %s", err) + } +} diff --git a/pkg/security/certificate_manager.go b/pkg/security/certificate_manager.go index 1caefe0c12a4..c0567fbfa7c0 100644 --- a/pkg/security/certificate_manager.go +++ b/pkg/security/certificate_manager.go @@ -265,6 +265,14 @@ func (cl CertsLocator) CACertPath() string { // CACertFilename returns the expected file name for the CA certificate. func CACertFilename() string { return "ca" + certExtension } +// CAKeyPath returns the expected file path for the CA certificate. +func (cl CertsLocator) CAKeyPath() string { + return filepath.Join(cl.certsDir, CAKeyFilename()) +} + +// CAKeyFilename returns the expected file name for the CA certificate. +func CAKeyFilename() string { return "ca" + keyExtension } + // TenantClientCACertPath returns the expected file path for the Tenant client CA // certificate. func (cl CertsLocator) TenantClientCACertPath() string { @@ -283,12 +291,24 @@ func (cl CertsLocator) ClientCACertPath() string { return filepath.Join(cl.certsDir, "ca-client"+certExtension) } +// ClientCAKeyPath returns the expected file path for the CA key +// used to sign client certificates. +func (cl CertsLocator) ClientCAKeyPath() string { + return filepath.Join(cl.certsDir, "ca-client"+keyExtension) +} + // UICACertPath returns the expected file path for the CA certificate // used to verify Admin UI certificates. func (cl CertsLocator) UICACertPath() string { return filepath.Join(cl.certsDir, "ca-ui"+certExtension) } +// UICAKeyPath returns the expected file path for the CA certificate +// used to verify Admin UI certificates. +func (cl CertsLocator) UICAKeyPath() string { + return filepath.Join(cl.certsDir, "ca-ui"+keyExtension) +} + // NodeCertPath returns the expected file path for the node certificate. func (cl CertsLocator) NodeCertPath() string { return filepath.Join(cl.certsDir, NodeCertFilename()) @@ -359,6 +379,98 @@ func ClientKeyFilename(user SQLUsername) string { return "client." + user.Normalized() + keyExtension } +// SQLServiceCertPath returns the expected file path for the +// SQL service certificate +func (cl CertsLocator) SQLServiceCertPath() string { + return filepath.Join(cl.certsDir, SQLServiceCertFilename()) +} + +// SQLServiceCertFilename returns the expected file name for the SQL service +// certificate +func SQLServiceCertFilename() string { + return "service.sql" + certExtension +} + +// SQLServiceKeyPath returns the expected file path for the SQL service key +func (cl CertsLocator) SQLServiceKeyPath() string { + return filepath.Join(cl.certsDir, SQLServiceKeyFilename()) +} + +// SQLServiceKeyFilename returns the expected file name for the SQL service +// certificate +func SQLServiceKeyFilename() string { + return "service.sql" + keyExtension +} + +// SQLServiceCACertPath returns the expected file path for the +// SQL CA certificate +func (cl CertsLocator) SQLServiceCACertPath() string { + return filepath.Join(cl.certsDir, SQLServiceCACertFilename()) +} + +// SQLServiceCACertFilename returns the expected file name for the SQL CA +// certificate +func SQLServiceCACertFilename() string { + return "service.ca.sql" + certExtension +} + +// SQLServiceCAKeyPath returns the expected file path for the SQL CA key +func (cl CertsLocator) SQLServiceCAKeyPath() string { + return filepath.Join(cl.certsDir, SQLServiceCAKeyFilename()) +} + +// SQLServiceCAKeyFilename returns the expected file name for the SQL CA +// certificate +func SQLServiceCAKeyFilename() string { + return "service.ca.sql" + keyExtension +} + +// RPCServiceCertPath returns the expected file path for the +// RPC service certificate +func (cl CertsLocator) RPCServiceCertPath() string { + return filepath.Join(cl.certsDir, RPCServiceCertFilename()) +} + +// RPCServiceCertFilename returns the expected file name for the RPC service +// certificate +func RPCServiceCertFilename() string { + return "service.rpc" + certExtension +} + +// RPCServiceKeyPath returns the expected file path for the RPC service key +func (cl CertsLocator) RPCServiceKeyPath() string { + return filepath.Join(cl.certsDir, RPCServiceKeyFilename()) +} + +// RPCServiceKeyFilename returns the expected file name for the RPC service +// certificate +func RPCServiceKeyFilename() string { + return "service.rpc" + keyExtension +} + +// RPCServiceCACertPath returns the expected file path for the +// RPC service certificate +func (cl CertsLocator) RPCServiceCACertPath() string { + return filepath.Join(cl.certsDir, RPCServiceCACertFilename()) +} + +// RPCServiceCACertFilename returns the expected file name for the RPC service +// certificate +func RPCServiceCACertFilename() string { + return "service.rpc" + certExtension +} + +// RPCServiceCAKeyPath returns the expected file path for the RPC service key +func (cl CertsLocator) RPCServiceCAKeyPath() string { + return filepath.Join(cl.certsDir, RPCServiceCAKeyFilename()) +} + +// RPCServiceCAKeyFilename returns the expected file name for the RPC service +// certificate +func RPCServiceCAKeyFilename() string { + return "service.rpc" + keyExtension +} + // CACert returns the CA cert. May be nil. // Callers should check for an internal Error field. func (cm *CertificateManager) CACert() *CertInfo { diff --git a/pkg/server/BUILD.bazel b/pkg/server/BUILD.bazel index b6e4de89af6d..ccc920eefd28 100644 --- a/pkg/server/BUILD.bazel +++ b/pkg/server/BUILD.bazel @@ -8,6 +8,7 @@ go_library( "api_auth.go", "api_error.go", "authentication.go", + "auto_tls_init.go", "auto_upgrade.go", "config.go", "config_unix.go", @@ -165,6 +166,7 @@ go_library( "@com_github_cockroachdb_circuitbreaker//:circuitbreaker", "@com_github_cockroachdb_cmux//:cmux", "@com_github_cockroachdb_errors//:errors", + "@com_github_cockroachdb_errors//oserror", "@com_github_cockroachdb_logtags//:logtags", "@com_github_cockroachdb_pebble//:pebble", "@com_github_cockroachdb_redact//:redact", @@ -234,6 +236,7 @@ go_test( "admin_cluster_test.go", "admin_test.go", "authentication_test.go", + "auto_tls_init_test.go", "config_test.go", "connectivity_test.go", "decommission_test.go", diff --git a/pkg/server/auto_tls_init.go b/pkg/server/auto_tls_init.go new file mode 100644 index 000000000000..edf62c6ff0f2 --- /dev/null +++ b/pkg/server/auto_tls_init.go @@ -0,0 +1,476 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +// TODO(aaron-crl): This uses the CertsLocator from the security package +// Getting about half way to integration with the certificate manager +// While I'd originally hoped to decouple it completely, I realized +// it would create an even larger headache if we maintained default +// certificate locations in multiple places. + +package server + +import ( + "io/ioutil" + "os" + "time" + + "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/security" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" +) + +// Define default certificate lifespan of 366 days +// TODO(aaron-crl): Put this in the config map. +const initLifespan = time.Minute * 60 * 24 * 366 + +// CertificateBundle manages the collection of certificates used by a +// CockroachDB node. +type CertificateBundle struct { + InterNode ServiceCertificateBundle + UserAuth ServiceCertificateBundle + SQLService ServiceCertificateBundle + RPCService ServiceCertificateBundle + AdminUIService ServiceCertificateBundle +} + +// ServiceCertificateBundle is a container for the CA and host node certs. +type ServiceCertificateBundle struct { + CACertificate []byte + CAKey []byte + HostCertificate []byte // This will be blank if unused (in the user case). + HostKey []byte // This will be blank if unused (in the user case). +} + +// Helper function to load cert and key for a service. +func (sb *ServiceCertificateBundle) loadServiceCertAndKey( + certPath string, keyPath string, +) (err error) { + sb.HostCertificate, err = loadCertificateFile(certPath) + if err != nil { + return + } + sb.HostKey, err = loadKeyFile(keyPath) + if err != nil { + return + } + return +} + +// Helper function to load cert and key for a service CA. +func (sb *ServiceCertificateBundle) loadCACertAndKey(certPath string, keyPath string) (err error) { + sb.CACertificate, err = loadCertificateFile(certPath) + if err != nil { + return + } + sb.CAKey, err = loadKeyFile(keyPath) + if err != nil { + return + } + return +} + +// LoadUserAuthCACertAndKey loads host certificate and key from disk or fails with error. +func (sb *ServiceCertificateBundle) loadOrCreateUserAuthCACertAndKey( + caCertPath string, caKeyPath string, initLifespan time.Duration, serviceName string, +) (err error) { + // Check if the service cert and key already exist. + if _, err = os.Stat(caCertPath); oserror.IsNotExist(err) { + // Cert DNE. + if _, err = os.Stat(caKeyPath); !oserror.IsNotExist(err) { + // Key exists but cert does not, this is an error. + return errors.Wrapf(err, + "found key but not certificate for user auth at: %s", caKeyPath) + } + + // Create both cert and key for service CA. + err = sb.createServiceCA(caCertPath, caKeyPath, initLifespan, serviceName) + if err != nil { + return err + } + + return nil + } + + // Load cert into ServiceCertificateBundle. + sb.CACertificate, err = loadCertificateFile(caCertPath) + if err != nil { + return err + } + + // Load the key only if it exists. + if _, err = os.Stat(caKeyPath); !oserror.IsNotExist(err) { + sb.CAKey, err = loadKeyFile(caKeyPath) + if err != nil { + return err + } + } + + return nil +} + +// loadOrCreateServiceCertificates will attempt to load the service cert/key +// into the service bundle. +// * If they do not exist: +// It will attempt to load the service CA cert/key pair. +// * If they do not exist: +// It will generate the service CA cert/key pair. +// It will persist these to disk and store them +// in the ServiceCertificateBundle. +// It will generate the service cert/key pair. +// It will persist these to disk and store them +// in the ServiceCertificateBundle. +func (sb *ServiceCertificateBundle) loadOrCreateServiceCertificates( + serviceCertPath string, + serviceKeyPath string, + caCertPath string, + caKeyPath string, + initLifespan time.Duration, + serviceName string, + hostname string, +) (err error) { + // Check if the service cert and key already exist. + if _, err = os.Stat(serviceCertPath); !oserror.IsNotExist(err) { + // cert exists + if _, err = os.Stat(serviceKeyPath); oserror.IsNotExist(err) { + // cert exists but key doesn't, this is an error + err = errors.Wrapf(err, + "failed to load service certificate key for %s expected key at %s", + serviceCertPath, serviceKeyPath) + return + } + } else { + // Niether service cert or key exist, attempt to load CA. + // Check if the CA cert and key already exist. + if _, err = os.Stat(caCertPath); !oserror.IsNotExist(err) { + // cert exists + if _, err = os.Stat(caKeyPath); oserror.IsNotExist(err) { + // cert exists but key doesn't, this is an error + err = errors.Wrapf(err, + "failed to load service CA key for %s expected key at %s", + caCertPath, caKeyPath) + return + } + + sb.CACertificate, err = loadCertificateFile(caCertPath) + if err != nil { + return errors.Wrapf( + err, "failed to load certificate file: %s", caCertPath, + ) + } + + sb.CAKey, err = loadKeyFile(caKeyPath) + if err != nil { + return errors.Wrapf( + err, "failed to load key file: %s", caKeyPath, + ) + } + + } else { + // Build the CA cert and key. + err = sb.createServiceCA(caCertPath, caKeyPath, initLifespan, serviceName) + if err != nil { + return errors.Wrap( + err, "failed to create Service CA", + ) + } + + } + + // Build service cert and key. + var hostCert, hostKey []byte + hostCert, hostKey, err = security.CreateServiceCertAndKey( + initLifespan, + serviceName, + hostname, + sb.CACertificate, + sb.CAKey, + ) + if err != nil { + return errors.Wrap( + err, "failed to create Service Cert and Key", + ) + } + + err = writeCertificateFile(serviceCertPath, hostCert) + if err != nil { + return err + } + + err = writeKeyFile(serviceKeyPath, hostKey) + if err != nil { + return err + } + + } + + sb.HostCertificate, err = loadCertificateFile(serviceCertPath) + if err != nil { + return err + } + + sb.HostKey, err = loadKeyFile(serviceKeyPath) + if err != nil { + return err + } + + return nil +} + +// createServiceCA builds CA cert and key and populates them to +// ServiceCertificateBundle. +func (sb *ServiceCertificateBundle) createServiceCA( + caCertPath string, caKeyPath string, initLifespan time.Duration, serviceName string, +) (err error) { + sb.CACertificate, sb.CAKey, err = security.CreateCACertAndKey(initLifespan, serviceName) + if err != nil { + return + } + + err = writeCertificateFile(caCertPath, sb.CACertificate) + if err != nil { + return + } + + err = writeKeyFile(caKeyPath, sb.CAKey) + if err != nil { + return + } + + return +} + +// Simple wrapper to make it easier to store certs somewhere else later. +// TODO (aaron-crl): Put validation checks here. +func loadCertificateFile(certPath string) (cert []byte, err error) { + cert, err = ioutil.ReadFile(certPath) + return +} + +// Simple wrapper to make it easier to store certs somewhere else later. +// TODO (aaron-crl): Put validation checks here. +func loadKeyFile(keyPath string) (key []byte, err error) { + key, err = ioutil.ReadFile(keyPath) + return +} + +// simple wrapper to make it easier to store certs somewhere else later. +func writeCertificateFile(certPath string, certPEM []byte) error { + return ioutil.WriteFile(certPath, certPEM, 0600) +} + +// simple wrapper to make it easier to store certs somewhere else later. +func writeKeyFile(keyPath string, keyPEM []byte) error { + return ioutil.WriteFile(keyPath, keyPEM, 0600) +} + +// InitializeFromConfig is called by the node creating certificates for the +// cluster. It uses or generates an InterNode CA to produce any missing +// unmanaged certificates. It does this base on the logic in: +// https://github.com/cockroachdb/cockroach/pull/51991 +// N.B.: This function fast fails if an InterNodeHost cert/key pair are present +// as this should _never_ happen. +func (b *CertificateBundle) InitializeFromConfig(c base.Config) (err error) { + cl := security.MakeCertsLocator(c.SSLCertsDir) + + // First check to see if host cert is already present + // if it is, we should fail to initialize. + if _, err = os.Stat(cl.NodeCertPath()); !oserror.IsNotExist(err) { + err = errors.New("interNodeHost certificate already present") + return + } + + // Start by loading or creating the InterNode certificates. + err = b.InterNode.loadOrCreateServiceCertificates( + cl.NodeCertPath(), + cl.NodeKeyPath(), + cl.CACertPath(), + cl.CAKeyPath(), + initLifespan, + "InterNode Service", + c.Addr, + ) + if err != nil { + err = errors.Wrap(err, + "failed to load or create InterNode certificates") + return + } + + // Initialize User auth certificates. + // TODO(aaron-crl): Double check that we want to do this. It seems + // like this is covered by the interface certificates? + err = b.UserAuth.loadOrCreateUserAuthCACertAndKey( + cl.ClientCACertPath(), + cl.ClientCAKeyPath(), + initLifespan, + "User Authentication", + ) + if err != nil { + err = errors.Wrap(err, + "failed to load or create User auth certificate(s)") + return + } + + // Initialize SQLService Certs. + err = b.SQLService.loadOrCreateServiceCertificates( + cl.SQLServiceCertPath(), + cl.SQLServiceKeyPath(), + cl.SQLServiceCACertPath(), + cl.SQLServiceCAKeyPath(), + initLifespan, + "SQL Service", + c.SQLAddr, + ) + if err != nil { + err = errors.Wrap(err, + "failed to load or create SQL service certificate(s)") + return + } + + // Initialize RPCService Certs. + err = b.RPCService.loadOrCreateServiceCertificates( + cl.RPCServiceCertPath(), + cl.RPCServiceKeyPath(), + cl.RPCServiceCACertPath(), + cl.RPCServiceCAKeyPath(), + initLifespan, + "RPC Service", + c.SQLAddr, // TODO(aaron-crl): Add RPC variable to config. + ) + if err != nil { + err = errors.Wrap(err, + "failed to load or create RPC service certificate(s)") + return + } + + // Initialize AdminUIService Certs. + err = b.AdminUIService.loadOrCreateServiceCertificates( + cl.UICertPath(), + cl.UIKeyPath(), + cl.UICACertPath(), + cl.UICAKeyPath(), + initLifespan, + "AdminUI Service", + c.HTTPAddr, + ) + if err != nil { + err = errors.Wrap(err, + "failed to load or create Admin UI service certificate(s)") + return + } + + return +} + +// InitializeNodeFromBundle uses the contents of the CertificateBundle and +// details from the config object to write certs to disk and generate any +// missing host-specific certificates and keys +// It is assumed that a node receiving this has not has TLS initialized. If +// a interNodeHost certificate is found, this function will error. +func (b *CertificateBundle) InitializeNodeFromBundle(c base.Config) (err error) { + cl := security.MakeCertsLocator(c.SSLCertsDir) + + // First check to see if host cert is already present + // if it is, we should fail to initialize. + if _, err = os.Stat(cl.NodeCertPath()); !oserror.IsNotExist(err) { + err = errors.New("interNodeHost certificate already present") + return + } + + // Write received CA's to disk. If any of them already exist, fail + // and return an error. + + // Attempt to write InterNodeHostCA to disk first. + err = b.InterNode.writeCAOrFail(cl.CACertPath(), cl.CAKeyPath()) + if err != nil { + err = errors.Wrap(err, "failed to write InterNodeCA to disk") + return + } + + // Attempt to write ClientCA to disk. + err = b.InterNode.writeCAOrFail(cl.ClientCACertPath(), cl.ClientCAKeyPath()) + if err != nil { + err = errors.Wrap(err, "failed to write ClientCA to disk") + return + } + + // Attempt to write SQLServiceCA to disk. + err = b.InterNode.writeCAOrFail(cl.SQLServiceCACertPath(), cl.SQLServiceCAKeyPath()) + if err != nil { + err = errors.Wrap(err, "failed to write SQLServiceCA to disk") + return + } + + // Attempt to write RPCServiceCA to disk. + err = b.InterNode.writeCAOrFail(cl.RPCServiceCACertPath(), cl.RPCServiceCAKeyPath()) + if err != nil { + err = errors.Wrap(err, "failed to write RPCServiceCA to disk") + return + } + + // Attempt to write AdminUIServiceCA to disk. + err = b.InterNode.writeCAOrFail(cl.UICACertPath(), cl.UICAKeyPath()) + if err != nil { + err = errors.Wrap(err, "failed to write AdminUIServiceCA to disk") + return + } + + // Once CAs are written call the same InitFromConfig function to create + // host certificates. + err = b.InitializeFromConfig(c) + if err != nil { + err = errors.Wrap( + err, + "failed to initialize host certs after writing CAs to disk") + return + } + + return +} + +// writeCAOrFail will attempt to write a service certificate bundle to the +// specified paths on disk. It will ignore any missing certificate fields but +// error if it fails to write a file to disk. +func (sb *ServiceCertificateBundle) writeCAOrFail(certPath string, keyPath string) (err error) { + if sb.CACertificate != nil { + err = writeCertificateFile(certPath, sb.CACertificate) + if err != nil { + return + } + } + + if sb.CAKey != nil { + err = writeKeyFile(keyPath, sb.CAKey) + if err != nil { + return + } + } + + return +} + +// copyOnlyCAs is a helper function to only populate the CA portion of +// a ServiceCertificateBundle +func (sb *ServiceCertificateBundle) copyOnlyCAs(destBundle *ServiceCertificateBundle) { + destBundle.CACertificate = sb.CACertificate + destBundle.CAKey = sb.CAKey +} + +// ToPeerInitBundle populates a bundle of initialization certificate CAs (only). +// This function is expected to serve any node providing a init bundle to a +// joining or starting peer. +func (b *CertificateBundle) ToPeerInitBundle() (pb CertificateBundle) { + b.InterNode.copyOnlyCAs(&pb.InterNode) + b.UserAuth.copyOnlyCAs(&pb.UserAuth) + b.SQLService.copyOnlyCAs(&pb.SQLService) + b.RPCService.copyOnlyCAs(&pb.RPCService) + b.AdminUIService.copyOnlyCAs(&pb.AdminUIService) + return +} diff --git a/pkg/server/auto_tls_init_test.go b/pkg/server/auto_tls_init_test.go new file mode 100644 index 000000000000..0a74688b081e --- /dev/null +++ b/pkg/server/auto_tls_init_test.go @@ -0,0 +1,91 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package server + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" +) + +// TestDummyInitializeFromConfig is a placeholder for actual testing functions. +// TODO(aaron-crl): [tests] write unit tests. +func TestDummyInitializeFromConfig(t *testing.T) { + defer leaktest.AfterTest(t)() + + // Create a temp dir for all certificate tests. + tempDir, err := ioutil.TempDir("", "auto_tls_init_test") + if err != nil { + t.Fatalf("failed to create test temp dir: %s", err) + } + + certBundle := CertificateBundle{} + cfg := base.Config{ + SSLCertsDir: tempDir, + } + + err = certBundle.InitializeFromConfig(cfg) + if err != nil { + t.Fatalf("expected err=nil, got: %s", err) + } + + // Remove temp directory now that we are done with it. + err = os.RemoveAll(tempDir) + if err != nil { + t.Fatalf("failed to remove test temp dir: %s", err) + } + +} + +// TestDummyInitializeNodeFromBundle is a placeholder for actual testing functions. +// TODO(aaron-crl): [tests] write unit tests. +func TestDummyInitializeNodeFromBundle(t *testing.T) { + defer leaktest.AfterTest(t)() + + // Create a temp dir for all certificate tests. + tempDir, err := ioutil.TempDir("", "auto_tls_init_test") + if err != nil { + t.Fatalf("failed to create test temp dir: %s", err) + } + + certBundle := CertificateBundle{} + cfg := base.Config{ + SSLCertsDir: tempDir, + } + + err = certBundle.InitializeNodeFromBundle(cfg) + if err != nil { + t.Fatalf("expected err=nil, got: %s", err) + } + + // Remove temp directory now that we are done with it. + err = os.RemoveAll(tempDir) + if err != nil { + t.Fatalf("failed to remove test temp dir: %s", err) + } +} + +// TestDummyCertLoader is a placeholder for actual testing functions. +// TODO(aaron-crl): [tests] write unit tests. +func TestDummyCertLoader(t *testing.T) { + defer leaktest.AfterTest(t)() + + scb := ServiceCertificateBundle{} + _ = scb.loadServiceCertAndKey("", "") + _ = scb.loadCACertAndKey("", "") + + cb := CertificateBundle{} + cb.InterNode.copyOnlyCAs(&scb) + _ = cb.ToPeerInitBundle() +}