Add flags to support CA generation for Connect (#9585)

This commit is contained in:
Hans Hasselberg 2021-01-27 08:52:15 +01:00 committed by GitHub
parent 38d630e2e9
commit 623aab5880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 130 additions and 96 deletions

3
.changelog/9585.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Add new `-cluster-id` and `common-name` to `consul tls ca create` to support creating a CA for Consul Connect.
```

View File

@ -2,9 +2,6 @@ package consul
import ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net" "net"
@ -46,22 +43,17 @@ const (
// testTLSCertificates Generates a TLS CA and server key/cert and returns them // testTLSCertificates Generates a TLS CA and server key/cert and returns them
// in PEM encoded form. // in PEM encoded form.
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) { func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA signer, _, err := tlsutil.GeneratePrivateKey()
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
// generate leaf ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
serial, err = tlsutil.GenerateSerialNumber() if err != nil {
return "", "", "", err
}
serial, err := tlsutil.GenerateSerialNumber()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@ -1,9 +1,6 @@
package pool package pool
import ( import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
@ -194,22 +191,18 @@ func deadlineNetPipe(deadline time.Time) (net.Conn, net.Conn, error) {
} }
func generateTestCert(serverName string) (cert tls.Certificate, caPEM []byte, err error) { func generateTestCert(serverName string) (cert tls.Certificate, caPEM []byte, err error) {
// generate CA signer, _, err := tlsutil.GeneratePrivateKey()
serial, err := tlsutil.GenerateSerialNumber()
if err != nil { if err != nil {
return tls.Certificate{}, nil, err return tls.Certificate{}, nil, err
} }
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
return tls.Certificate{}, nil, err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
if err != nil { if err != nil {
return tls.Certificate{}, nil, err return tls.Certificate{}, nil, err
} }
// generate leaf // generate leaf
serial, err = tlsutil.GenerateSerialNumber() serial, err := tlsutil.GenerateSerialNumber()
if err != nil { if err != nil {
return tls.Certificate{}, nil, err return tls.Certificate{}, nil, err
} }

View File

@ -1,9 +1,6 @@
package leakcheck package leakcheck
import ( import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509" "crypto/x509"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
@ -18,22 +15,18 @@ import (
) )
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) { func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{})
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
// generate leaf // generate leaf
serial, err = tlsutil.GenerateSerialNumber() serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, _, err := tlsutil.GeneratePrivateKey()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@ -3,8 +3,6 @@ package agent
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io" "io"
@ -569,22 +567,17 @@ func TestACLConfigWithParams(params *TestACLConfigParams) string {
// testTLSCertificates Generates a TLS CA and server key/cert and returns them // testTLSCertificates Generates a TLS CA and server key/cert and returns them
// in PEM encoded form. // in PEM encoded form.
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) { func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA signer, _, err := tlsutil.GeneratePrivateKey()
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.New(rand.NewSource(99)))
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
// generate leaf ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
serial, err = tlsutil.GenerateSerialNumber() if err != nil {
return "", "", "", err
}
serial, err := tlsutil.GenerateSerialNumber()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@ -23,7 +23,9 @@ type cmd struct {
help string help string
days int days int
domain string domain string
clusterID string
constraint bool constraint bool
commonName string
additionalConstraints flags.AppendSliceValue additionalConstraints flags.AppendSliceValue
} }
@ -36,6 +38,8 @@ func (c *cmd) init() {
"DNS. If the UI is going to be served over HTTPS its DNS has to be added with -additional-constraint. It is not "+ "DNS. If the UI is going to be served over HTTPS its DNS has to be added with -additional-constraint. It is not "+
"possible to add that after the fact! Defaults to false.") "possible to add that after the fact! Defaults to false.")
c.flags.StringVar(&c.domain, "domain", "consul", "Domain of consul cluster. Only used in combination with -name-constraint. Defaults to consul.") c.flags.StringVar(&c.domain, "domain", "consul", "Domain of consul cluster. Only used in combination with -name-constraint. Defaults to consul.")
c.flags.StringVar(&c.clusterID, "cluster-id", "", "ClusterID of the consul cluster, requires -domain to be set as well. When used will set URIs with spiffeid.")
c.flags.StringVar(&c.commonName, "common-name", "", "Common Name of CA. Defaults to Consul Agent CA.")
c.flags.Var(&c.additionalConstraints, "additional-name-constraint", "Add name constraints for the CA. Results in rejecting certificates "+ c.flags.Var(&c.additionalConstraints, "additional-name-constraint", "Add name constraints for the CA. Results in rejecting certificates "+
"for other DNS than specified. Can be used multiple times. Only used in combination with -name-constraint.") "for other DNS than specified. Can be used multiple times. Only used in combination with -name-constraint.")
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
@ -62,23 +66,12 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
sn, err := tlsutil.GenerateSerialNumber()
if err != nil {
c.UI.Error(err.Error())
return 1
}
s, pk, err := tlsutil.GeneratePrivateKey()
if err != nil {
c.UI.Error(err.Error())
return 1
}
constraints := []string{} constraints := []string{}
if c.constraint { if c.constraint {
constraints = append(c.additionalConstraints, []string{c.domain, "localhost"}...) constraints = append(c.additionalConstraints, []string{c.domain, "localhost"}...)
} }
ca, err := tlsutil.GenerateCA(s, sn, c.days, constraints) ca, pk, err := tlsutil.GenerateCA(tlsutil.CAOpts{Name: c.commonName, Days: c.days, Domain: c.domain, PermittedDNSDomains: constraints, ClusterID: c.clusterID})
if err != nil { if err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 1 return 1

View File

@ -62,22 +62,53 @@ func TestCACreateCommand(t *testing.T) {
require.ElementsMatch(t, cert.PermittedDNSDomains, []string{"foo", "localhost", "bar"}) require.ElementsMatch(t, cert.PermittedDNSDomains, []string{"foo", "localhost", "bar"})
}, },
}, },
{"with cluster-id",
[]string{
"-domain=foo",
"-cluster-id=uuid",
},
"foo-agent-ca.pem",
"foo-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.Len(t, cert.URIs, 1)
require.Equal(t, cert.URIs[0].String(), "spiffe://uuid.foo")
},
},
{"with common-name",
[]string{
"-common-name=foo",
},
"consul-agent-ca.pem",
"consul-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.Equal(t, cert.Subject.CommonName, "foo")
},
},
{"without common-name",
[]string{},
"consul-agent-ca.pem",
"consul-agent-ca-key.pem",
func(t *testing.T, cert *x509.Certificate) {
require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA"))
},
},
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc tc := tc
require.True(t, t.Run(tc.name, func(t *testing.T) { require.True(t, t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui) cmd := New(ui)
require.Equal(t, 0, cmd.Run(tc.args)) require.Equal(t, 0, cmd.Run(tc.args), ui.ErrorWriter.String())
require.Equal(t, "", ui.ErrorWriter.String()) require.Equal(t, "", ui.ErrorWriter.String())
cert, _ := expectFiles(t, tc.caPath, tc.keyPath) cert, _ := expectFiles(t, tc.caPath, tc.keyPath)
require.Contains(t, cert.Subject.CommonName, "Consul Agent CA")
require.True(t, cert.BasicConstraintsValid) require.True(t, cert.BasicConstraintsValid)
require.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageDigitalSignature, cert.KeyUsage) require.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageDigitalSignature, cert.KeyUsage)
require.True(t, cert.IsCA) require.True(t, cert.IsCA)
require.Equal(t, cert.AuthorityKeyId, cert.SubjectKeyId) require.Equal(t, cert.AuthorityKeyId, cert.SubjectKeyId)
tc.extraCheck(t, cert) tc.extraCheck(t, cert)
require.NoError(t, os.Remove(tc.caPath))
require.NoError(t, os.Remove(tc.keyPath))
})) }))
} }

View File

@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
"net/url"
) )
// GenerateSerialNumber returns random bigint generated with crypto/rand // GenerateSerialNumber returns random bigint generated with crypto/rand
@ -32,18 +33,61 @@ func GeneratePrivateKey() (crypto.Signer, string, error) {
return connect.GeneratePrivateKey() return connect.GeneratePrivateKey()
} }
type CAOpts struct {
Signer crypto.Signer
Serial *big.Int
ClusterID string
Days int
PermittedDNSDomains []string
Domain string
Name string
}
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS) // GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
func GenerateCA(signer crypto.Signer, sn *big.Int, days int, constraints []string) (string, error) { func GenerateCA(opts CAOpts) (string, string, error) {
id, err := keyID(signer.Public()) signer := opts.Signer
if err != nil { var pk string
return "", err if signer == nil {
var err error
signer, pk, err = GeneratePrivateKey()
if err != nil {
return "", "", err
}
} }
name := fmt.Sprintf("Consul Agent CA %d", sn) id, err := keyID(signer.Public())
if err != nil {
return "", "", err
}
sn := opts.Serial
if sn == nil {
var err error
sn, err = GenerateSerialNumber()
if err != nil {
return "", "", err
}
}
name := opts.Name
if name == "" {
name = fmt.Sprintf("Consul Agent CA %d", sn)
}
days := opts.Days
if opts.Days == 0 {
days = 365
}
var uris []*url.URL
if opts.ClusterID != "" {
spiffeID := connect.SpiffeIDSigning{ClusterID: opts.ClusterID, Domain: opts.Domain}
uris = []*url.URL{spiffeID.URI()}
}
// Create the CA cert // Create the CA cert
template := x509.Certificate{ template := x509.Certificate{
SerialNumber: sn, SerialNumber: sn,
URIs: uris,
Subject: pkix.Name{ Subject: pkix.Name{
Country: []string{"US"}, Country: []string{"US"},
PostalCode: []string{"94105"}, PostalCode: []string{"94105"},
@ -62,23 +106,23 @@ func GenerateCA(signer crypto.Signer, sn *big.Int, days int, constraints []strin
SubjectKeyId: id, SubjectKeyId: id,
} }
if len(constraints) > 0 { if len(opts.PermittedDNSDomains) > 0 {
template.PermittedDNSDomainsCritical = true template.PermittedDNSDomainsCritical = true
template.PermittedDNSDomains = constraints template.PermittedDNSDomains = opts.PermittedDNSDomains
} }
bs, err := x509.CreateCertificate( bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, signer.Public(), signer) rand.Reader, &template, &template, signer.Public(), signer)
if err != nil { if err != nil {
return "", fmt.Errorf("error generating CA certificate: %s", err) return "", "", fmt.Errorf("error generating CA certificate: %s", err)
} }
var buf bytes.Buffer var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil { if err != nil {
return "", fmt.Errorf("error encoding private key: %s", err) return "", "", fmt.Errorf("error encoding private key: %s", err)
} }
return buf.String(), nil return buf.String(), pk, nil
} }
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS) // GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)

View File

@ -8,13 +8,13 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt"
"io" "io"
"net" "net"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"strings"
) )
func TestSerialNumber(t *testing.T) { func TestSerialNumber(t *testing.T) {
@ -62,32 +62,26 @@ func (s *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts)
func TestGenerateCA(t *testing.T) { func TestGenerateCA(t *testing.T) {
t.Parallel() t.Parallel()
sn, err := GenerateSerialNumber() ca, pk, err := GenerateCA(CAOpts{Signer: &TestSigner{}})
require.Nil(t, err)
var s crypto.Signer
// test what happens without key
s = &TestSigner{}
ca, err := GenerateCA(s, sn, 0, nil)
require.Error(t, err) require.Error(t, err)
require.Empty(t, ca) require.Empty(t, ca)
require.Empty(t, pk)
// test what happens with wrong key // test what happens with wrong key
s = &TestSigner{public: &rsa.PublicKey{}} ca, pk, err = GenerateCA(CAOpts{Signer: &TestSigner{public: &rsa.PublicKey{}}})
ca, err = GenerateCA(s, sn, 0, nil)
require.Error(t, err) require.Error(t, err)
require.Empty(t, ca) require.Empty(t, ca)
require.Empty(t, pk)
// test what happens with correct key // test what happens with correct key
s, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) ca, pk, err = GenerateCA(CAOpts{})
require.Nil(t, err)
ca, err = GenerateCA(s, sn, 365, nil)
require.Nil(t, err) require.Nil(t, err)
require.NotEmpty(t, ca) require.NotEmpty(t, ca)
require.NotEmpty(t, pk)
cert, err := parseCert(ca) cert, err := parseCert(ca)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, fmt.Sprintf("Consul Agent CA %d", sn), cert.Subject.CommonName) require.True(t, strings.HasPrefix(cert.Subject.CommonName, "Consul Agent CA"))
require.Equal(t, true, cert.IsCA) require.Equal(t, true, cert.IsCA)
require.Equal(t, true, cert.BasicConstraintsValid) require.Equal(t, true, cert.BasicConstraintsValid)
@ -99,14 +93,12 @@ func TestGenerateCA(t *testing.T) {
func TestGenerateCert(t *testing.T) { func TestGenerateCert(t *testing.T) {
t.Parallel() t.Parallel()
sn, err := GenerateSerialNumber()
require.Nil(t, err)
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.Nil(t, err) require.Nil(t, err)
ca, err := GenerateCA(signer, sn, 365, nil) ca, _, err := GenerateCA(CAOpts{Signer: signer})
require.Nil(t, err) require.Nil(t, err)
sn, err = GenerateSerialNumber() sn, err := GenerateSerialNumber()
require.Nil(t, err) require.Nil(t, err)
DNSNames := []string{"server.dc1.consul"} DNSNames := []string{"server.dc1.consul"}
IPAddresses := []net.IP{net.ParseIP("123.234.243.213")} IPAddresses := []net.IP{net.ParseIP("123.234.243.213")}