Skip to content

Commit

Permalink
security: add CA and client certs for tenant usage
Browse files Browse the repository at this point in the history
This commit introduces methods that create a CA certificate for use with
the auth broker envisioned in cockroachdb#49105 and, from that CA, client
certificates for use in cockroachdb#47898. They are not exposed through the cli
(yet), though the certificate loader already supports them.

Touches cockroachdb#49105.
Touches cockroachdb#47898.

Release note: None
  • Loading branch information
tbg committed Jun 4, 2020
1 parent fd1bccf commit ad18650
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 28 deletions.
23 changes: 21 additions & 2 deletions pkg/security/certificate_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ const (
_ PemUsage = iota
// CAPem describes the main CA certificate.
CAPem
// TenantCAPem describes the CA certificate used to broker authN/Z for SQL
// tenants wishing to access the KV layer.
TenantCAPem
// ClientCAPem describes the CA certificate used to verify client certificates.
ClientCAPem
// UICAPem describes the CA certificate used to verify the Admin UI server certificate.
Expand All @@ -83,6 +86,8 @@ const (
UIPem
// ClientPem describes a client certificate.
ClientPem
// TenantClientPem describes a SQL tenant client certificate.
TenantClientPem

// Maximum allowable permissions.
maxKeyPermissions os.FileMode = 0700
Expand All @@ -94,7 +99,7 @@ const (
)

func isCA(usage PemUsage) bool {
return usage == CAPem || usage == ClientCAPem || usage == UICAPem
return usage == CAPem || usage == ClientCAPem || usage == TenantCAPem || usage == UICAPem
}

func (p PemUsage) String() string {
Expand All @@ -103,6 +108,8 @@ func (p PemUsage) String() string {
return "CA"
case ClientCAPem:
return "Client CA"
case TenantCAPem:
return "Tenant CA"
case UICAPem:
return "UI CA"
case NodePem:
Expand Down Expand Up @@ -185,6 +192,11 @@ func CertInfoFromFilename(filename string) (*CertInfo, error) {
if numParts != 2 {
return nil, errors.Errorf("client CA certificate filename should match ca-client%s", certExtension)
}
case `ca-tenant`:
fileUsage = TenantCAPem
if numParts != 2 {
return nil, errors.Errorf("tenant CA certificate filename should match ca%s", certExtension)
}
case `ca-ui`:
fileUsage = UICAPem
if numParts != 2 {
Expand All @@ -202,11 +214,18 @@ func CertInfoFromFilename(filename string) (*CertInfo, error) {
}
case `client`:
fileUsage = ClientPem
// strip prefix and suffix and re-join middle parts.
// Strip prefix and suffix and re-join middle parts.
name = strings.Join(parts[1:numParts-1], `.`)
if len(name) == 0 {
return nil, errors.Errorf("client certificate filename should match client.<user>%s", certExtension)
}
case `tenant`:
fileUsage = TenantClientPem
// Strip prefix and suffix and re-join middle parts.
name = strings.Join(parts[1:numParts-1], `.`)
if len(name) == 0 {
return nil, errors.Errorf("tenant certificate filename should match tenant.<tenantid>%s", certExtension)
}
default:
return nil, errors.Errorf("unknown prefix %q", prefix)
}
Expand Down
46 changes: 44 additions & 2 deletions pkg/security/certificate_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ func (cm *CertificateManager) CACertPath() string {
// CACertFilename returns the expected file name for the CA certificate.
func CACertFilename() string { return "ca" + certExtension }

// TenantCACertPath returns the expected file path for the Tenant CA
// certificate.
func (cm *CertificateManager) TenantCACertPath() string {
return filepath.Join(cm.certsDir, TenantCACertFilename())
}

// TenantCACertFilename returns the expected file name for the Tenant CA
// certificate.
func TenantCACertFilename() string {
return "ca-tenant" + certExtension
}

// ClientCACertPath returns the expected file path for the CA certificate
// used to verify client certificates.
func (cm *CertificateManager) ClientCACertPath() string {
Expand All @@ -209,12 +221,22 @@ func (cm *CertificateManager) UICACertPath() string {

// NodeCertPath returns the expected file path for the node certificate.
func (cm *CertificateManager) NodeCertPath() string {
return filepath.Join(cm.certsDir, "node"+certExtension)
return filepath.Join(cm.certsDir, NodeCertFilename())
}

// NodeCertFilename returns the expected file name for the node certificate.
func NodeCertFilename() string {
return "node" + certExtension
}

// NodeKeyPath returns the expected file path for the node key.
func (cm *CertificateManager) NodeKeyPath() string {
return filepath.Join(cm.certsDir, "node"+keyExtension)
return filepath.Join(cm.certsDir, NodeKeyFilename())
}

// NodeKeyFilename returns the expected file name for the node key.
func NodeKeyFilename() string {
return "node" + keyExtension
}

// UICertPath returns the expected file path for the UI certificate.
Expand All @@ -227,6 +249,26 @@ func (cm *CertificateManager) UIKeyPath() string {
return filepath.Join(cm.certsDir, "ui"+keyExtension)
}

// TenantClientCertPath returns the expected file path for the user's certificate.
func (cm *CertificateManager) TenantClientCertPath(tenantIdentifier string) string {
return filepath.Join(cm.certsDir, TenantClientCertFilename(tenantIdentifier))
}

// TenantClientCertFilename returns the expected file name for the user's certificate.
func TenantClientCertFilename(tenantIdentifier string) string {
return "tenant." + tenantIdentifier + certExtension
}

// TenantClientKeyPath returns the expected file path for the tenant's key.
func (cm *CertificateManager) TenantClientKeyPath(tenantIdentifier string) string {
return filepath.Join(cm.certsDir, TenantClientKeyFilename(tenantIdentifier))
}

// TenantClientKeyFilename returns the expected file name for the user's key.
func TenantClientKeyFilename(tenantIdentifier string) string {
return "tenant." + tenantIdentifier + keyExtension
}

// ClientCertPath returns the expected file path for the user's certificate.
func (cm *CertificateManager) ClientCertPath(user string) string {
return filepath.Join(cm.certsDir, ClientCertFilename(user))
Expand Down
105 changes: 104 additions & 1 deletion pkg/security/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ func CreateCAPair(
return createCACertAndKey(certsDir, caKeyPath, CAPem, keySize, lifetime, allowKeyReuse, overwrite)
}

// CreateTenantCAPair creates a tenant CA pair. The private key is written to
// caKeyPath and the public key is created in certsDir.
func CreateTenantCAPair(
certsDir, caKeyPath string,
keySize int,
lifetime time.Duration,
allowKeyReuse bool,
overwrite bool,
) error {
return createCACertAndKey(certsDir, caKeyPath, TenantCAPem, keySize, lifetime, allowKeyReuse, overwrite)
}

// CreateClientCAPair creates a client CA certificate and associated key.
func CreateClientCAPair(
certsDir, caKeyPath string,
Expand Down Expand Up @@ -132,7 +144,7 @@ func createCACertAndKey(
if len(certsDir) == 0 {
return errors.New("the path to the certs directory is required")
}
if caType != CAPem && caType != ClientCAPem && caType != UICAPem {
if caType != CAPem && caType != TenantCAPem && caType != ClientCAPem && caType != UICAPem {
return fmt.Errorf("caType argument to createCACertAndKey must be one of CAPem (%d), ClientCAPem (%d), or UICAPem (%d), got: %d",
CAPem, ClientCAPem, UICAPem, caType)
}
Expand Down Expand Up @@ -194,10 +206,14 @@ func createCACertAndKey(
switch caType {
case CAPem:
certPath = cm.CACertPath()
case TenantCAPem:
certPath = cm.TenantCACertPath()
case ClientCAPem:
certPath = cm.ClientCACertPath()
case UICAPem:
certPath = cm.UICACertPath()
default:
return errors.Newf("unknown CA type %v", caType)
}

var existingCertificates []*pem.Block
Expand Down Expand Up @@ -424,6 +440,93 @@ func CreateClientPair(
return nil
}

// TenantClientPair are client certs for use with multi-tenancy.
type TenantClientPair struct {
PrivateKey *rsa.PrivateKey
Cert []byte
}

// CreateTenantClientPair creates a key and certificate for use as client certs
// when communicating with the KV layer. The tenant CA cert and key must load
// properly. If multiple certificates exist in the CA cert, the first one is
// used.
//
// To write the returned TenantClientPair to disk, use WriteTenantClientPair.
func CreateTenantClientPair(
certsDir, caKeyPath string,
keySize int,
lifetime time.Duration,
tenantIdentifier string,
hosts []string,
) (*TenantClientPair, error) {
if len(caKeyPath) == 0 {
return nil, errors.New("the path to the CA key is required")
}
if len(certsDir) == 0 {
return nil, errors.New("the path to the certs directory is required")
}

// The certificate manager expands the env for the certs directory.
// For consistency, we need to do this for the key as well.
caKeyPath = os.ExpandEnv(caKeyPath)

// Create a certificate manager with "create dir if not exist".
cm, err := NewCertificateManagerFirstRun(certsDir)
if err != nil {
return nil, err
}

caCertPath := cm.TenantCACertPath()

// Load the CA pair.
caCert, caPrivateKey, err := loadCACertAndKey(caCertPath, caKeyPath)
if err != nil {
return nil, err
}

// Generate certificates and keys.
clientKey, err := rsa.GenerateKey(rand.Reader, keySize)
if err != nil {
return nil, errors.Errorf("could not generate new tenant key: %v", err)
}

clientCert, err := GenerateClientCert(
caCert, caPrivateKey, clientKey.Public(), lifetime, tenantIdentifier, hosts...,
)
if err != nil {
return nil, errors.Errorf("error creating tenant certificate and key: %s", err)
}
return &TenantClientPair{
PrivateKey: clientKey,
Cert: clientCert,
}, nil
}

// WriteTenantClientPair writes a TenantClientPair into certsDir.
func WriteTenantClientPair(certsDir string, cp *TenantClientPair, overwrite bool) error {
cm, err := NewCertificateManagerFirstRun(certsDir)
if err != nil {
return err
}
cert, err := x509.ParseCertificate(cp.Cert)
if err != nil {
return err
}
tenantIdentifier := cert.Subject.CommonName
certPath := cm.TenantClientCertPath(tenantIdentifier)
if err := writeCertificateToFile(certPath, cp.Cert, overwrite); err != nil {
return errors.Errorf("error writing tenant certificate to %s: %v", certPath, err)
}
log.Infof(context.Background(), "Wrote SQL tenant client certificate: %s", certPath)

keyPath := cm.TenantClientKeyPath(tenantIdentifier)
if err := writeKeyToFile(keyPath, cp.PrivateKey, overwrite); err != nil {
return errors.Errorf("error writing tenant key to %s: %v", keyPath, err)
}
log.Infof(context.Background(), "Generated tenant key: %s", keyPath)
return nil
}

// PEMContentsToX509 takes raw pem-encoded contents and attempts to parse into
// x509.Certificate objects.
func PEMContentsToX509(contents []byte) ([]*x509.Certificate, error) {
Expand Down
Loading

0 comments on commit ad18650

Please sign in to comment.