internal/stack/certs.go (153 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package stack
import (
"bytes"
"crypto/x509"
"fmt"
"io"
"path/filepath"
"github.com/elastic/go-resource"
"github.com/elastic/elastic-package/internal/certs"
)
type tlsService struct {
Name string
IsClient bool
}
// tlsServices is the list of server TLS certificates that will be
// created in the given path.
var tlsServices = []tlsService{
{Name: "elasticsearch"},
{Name: "kibana"},
{Name: "package-registry"},
{Name: "fleet-server"},
{Name: "logstash"},
{Name: "elastic-agent", IsClient: true},
}
// tlsLocalServices is the list of server TLS certificates that will
// be created for local services when the stack is not local.
var tlsLocalServices = []tlsService{
{Name: "elastic-agent", IsClient: true},
{Name: "fleet-server"},
{Name: "logstash"},
}
var (
// CertificatesDirectory is the path to the certificates directory inside a profile.
CertificatesDirectory = "certs"
// CACertificateFile is the path to the CA certificate file inside a profile.
CACertificateFile = filepath.Join(CertificatesDirectory, "ca-cert.pem")
// CAKeyFile is the path to the CA key file inside a profile.
CAKeyFile = filepath.Join(CertificatesDirectory, "ca-key.pem")
// CAEnvFile is the path to the file with environment variables about the CA.
CAEnvFile = filepath.Join(CertificatesDirectory, "ca.env")
)
// initTLSCertificates initializes all the certificates needed to run the services
// managed by elastic-package stack. It includes a CA, and a pair of keys and
// certificates for each service.
func initTLSCertificates(fileProvider string, profilePath string, tlsServices []tlsService) ([]resource.Resource, error) {
certsDir := filepath.Join(profilePath, CertificatesDirectory)
caCertFile := filepath.Join(profilePath, string(CACertificateFile))
caKeyFile := filepath.Join(profilePath, string(CAKeyFile))
envFile := filepath.Join(profilePath, string(CAEnvFile))
var resources []resource.Resource
ca, err := initCA(caCertFile, caKeyFile)
if err != nil {
return nil, err
}
resources, err = certWriteToResource(resources, fileProvider, profilePath, caCertFile, ca.WriteCert)
if err != nil {
return nil, err
}
resources, err = certWriteToResource(resources, fileProvider, profilePath, caKeyFile, ca.WriteKey)
if err != nil {
return nil, err
}
resources, err = certWriteToResource(resources, fileProvider, profilePath, envFile, ca.WriteEnv)
if err != nil {
return nil, err
}
for _, service := range tlsServices {
certsDir := filepath.Join(certsDir, service.Name)
caFile := filepath.Join(certsDir, "ca-cert.pem")
certFile := filepath.Join(certsDir, "cert.pem")
keyFile := filepath.Join(certsDir, "key.pem")
cert, err := initServiceTLSCertificates(ca, caCertFile, certFile, keyFile, service)
if err != nil {
return nil, err
}
resources, err = certWriteToResource(resources, fileProvider, profilePath, certFile, cert.WriteCert)
if err != nil {
return nil, err
}
resources, err = certWriteToResource(resources, fileProvider, profilePath, keyFile, cert.WriteKey)
if err != nil {
return nil, err
}
// Write the CA also in the service directory, so only a directory needs to be mounted
// for services that need to configure the CA to validate other services certificates.
resources, err = certWriteToResource(resources, fileProvider, profilePath, caFile, ca.WriteCert)
if err != nil {
return nil, err
}
}
return resources, nil
}
func certWriteToResource(resources []resource.Resource, fileProvider string, profilePath string, absPath string, write func(w io.Writer) error) ([]resource.Resource, error) {
path, err := filepath.Rel(profilePath, absPath)
if err != nil {
return resources, err
}
var buf bytes.Buffer
err = write(&buf)
if err != nil {
return resources, err
}
return append(resources, &resource.File{
Provider: fileProvider,
Path: path,
CreateParent: true,
Content: resource.FileContentLiteral(buf.String()),
}), nil
}
func initCA(certFile, keyFile string) (*certs.Issuer, error) {
if err := verifyTLSCertificates(certFile, certFile, keyFile, tlsService{}); err == nil {
// Valid CA is already present, load it to check service certificates.
ca, err := certs.LoadCA(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("error loading CA: %w", err)
}
return ca, nil
}
ca, err := certs.NewCA()
if err != nil {
return nil, fmt.Errorf("error initializing self-signed CA")
}
return ca, nil
}
func initServiceTLSCertificates(ca *certs.Issuer, caCertFile string, certFile, keyFile string, service tlsService) (*certs.Certificate, error) {
if err := verifyTLSCertificates(caCertFile, certFile, keyFile, service); err == nil {
// Certificate already present and valid, load it.
return certs.LoadCertificate(certFile, keyFile)
}
var cert *certs.Certificate
var err error
if service.IsClient {
cert, err = ca.IssueClient(certs.WithName(service.Name))
if err != nil {
return nil, fmt.Errorf("error initializing certificate for %q", service.Name)
}
} else {
cert, err = ca.Issue(certs.WithName(service.Name))
if err != nil {
return nil, fmt.Errorf("error initializing certificate for %q", service.Name)
}
}
return cert, nil
}
func verifyTLSCertificates(caFile, certFile, keyFile string, service tlsService) error {
cert, err := certs.LoadCertificate(certFile, keyFile)
if err != nil {
return err
}
certPool, err := certs.PoolWithCACertificate(caFile)
if err != nil {
return err
}
options := x509.VerifyOptions{
Roots: certPool,
}
if service.Name != "" {
options.DNSName = service.Name
}
// By default ExtKeyUsageServerAuth is add to KeyUsages
// See https://github.com/golang/go/blob/master/src/crypto/x509/verify.go#L193-L195
if service.IsClient {
options.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
}
err = cert.Verify(options)
if err != nil {
return err
}
return nil
}