mirror of https://github.com/status-im/consul.git
connect.Service based implementation after review feedback.
This commit is contained in:
parent
69d5efdbbd
commit
26e65f6bfd
|
@ -29,7 +29,7 @@ const testClusterID = "11111111-2222-3333-4444-555555555555"
|
||||||
|
|
||||||
// testCACounter is just an atomically incremented counter for creating
|
// testCACounter is just an atomically incremented counter for creating
|
||||||
// unique names for the CA certs.
|
// unique names for the CA certs.
|
||||||
var testCACounter uint64 = 0
|
var testCACounter uint64
|
||||||
|
|
||||||
// TestCA creates a test CA certificate and signing key and returns it
|
// TestCA creates a test CA certificate and signing key and returns it
|
||||||
// in the CARoot structure format. The returned CA will be set as Active = true.
|
// in the CARoot structure format. The returned CA will be set as Active = true.
|
||||||
|
@ -44,7 +44,8 @@ func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot {
|
||||||
result.Name = fmt.Sprintf("Test CA %d", atomic.AddUint64(&testCACounter, 1))
|
result.Name = fmt.Sprintf("Test CA %d", atomic.AddUint64(&testCACounter, 1))
|
||||||
|
|
||||||
// Create the private key we'll use for this CA cert.
|
// Create the private key we'll use for this CA cert.
|
||||||
signer := testPrivateKey(t, &result)
|
signer, keyPEM := testPrivateKey(t)
|
||||||
|
result.SigningKey = keyPEM
|
||||||
|
|
||||||
// The serial number for the cert
|
// The serial number for the cert
|
||||||
sn, err := testSerialNumber()
|
sn, err := testSerialNumber()
|
||||||
|
@ -125,9 +126,9 @@ func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot {
|
||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLeaf returns a valid leaf certificate for the named service with
|
// TestLeaf returns a valid leaf certificate and it's private key for the named
|
||||||
// the given CA Root.
|
// service with the given CA Root.
|
||||||
func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
|
func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) {
|
||||||
// Parse the CA cert and signing key from the root
|
// Parse the CA cert and signing key from the root
|
||||||
cert := root.SigningCert
|
cert := root.SigningCert
|
||||||
if cert == "" {
|
if cert == "" {
|
||||||
|
@ -137,7 +138,7 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error parsing CA cert: %s", err)
|
t.Fatalf("error parsing CA cert: %s", err)
|
||||||
}
|
}
|
||||||
signer, err := ParseSigner(root.SigningKey)
|
caSigner, err := ParseSigner(root.SigningKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error parsing signing key: %s", err)
|
t.Fatalf("error parsing signing key: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -156,6 +157,9 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
|
||||||
t.Fatalf("error generating serial number: %s", err)
|
t.Fatalf("error generating serial number: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Genereate fresh private key
|
||||||
|
pkSigner, pkPEM := testPrivateKey(t)
|
||||||
|
|
||||||
// Cert template for generation
|
// Cert template for generation
|
||||||
template := x509.Certificate{
|
template := x509.Certificate{
|
||||||
SerialNumber: sn,
|
SerialNumber: sn,
|
||||||
|
@ -173,14 +177,14 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
|
||||||
},
|
},
|
||||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
AuthorityKeyId: testKeyID(t, signer.Public()),
|
AuthorityKeyId: testKeyID(t, caSigner.Public()),
|
||||||
SubjectKeyId: testKeyID(t, signer.Public()),
|
SubjectKeyId: testKeyID(t, pkSigner.Public()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the certificate, PEM encode it and return that value.
|
// Create the certificate, PEM encode it and return that value.
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
bs, err := x509.CreateCertificate(
|
bs, err := x509.CreateCertificate(
|
||||||
rand.Reader, &template, caCert, signer.Public(), signer)
|
rand.Reader, &template, caCert, pkSigner.Public(), caSigner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating certificate: %s", err)
|
t.Fatalf("error generating certificate: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -189,7 +193,7 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
|
||||||
t.Fatalf("error encoding private key: %s", err)
|
t.Fatalf("error encoding private key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.String()
|
return buf.String(), pkPEM
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCSR returns a CSR to sign the given service along with the PEM-encoded
|
// TestCSR returns a CSR to sign the given service along with the PEM-encoded
|
||||||
|
@ -200,39 +204,22 @@ func TestCSR(t testing.T, uri CertURI) (string, string) {
|
||||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result buffers
|
|
||||||
var csrBuf, pkBuf bytes.Buffer
|
|
||||||
|
|
||||||
// Create the private key we'll use
|
// Create the private key we'll use
|
||||||
signer := testPrivateKey(t, nil)
|
signer, pkPEM := testPrivateKey(t)
|
||||||
|
|
||||||
{
|
// Create the CSR itself
|
||||||
// Create the private key PEM
|
var csrBuf bytes.Buffer
|
||||||
bs, err := x509.MarshalECPrivateKey(signer.(*ecdsa.PrivateKey))
|
bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error marshalling PK: %s", err)
|
t.Fatalf("error creating CSR: %s", err)
|
||||||
}
|
|
||||||
|
|
||||||
err = pem.Encode(&pkBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error encoding PK: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
|
||||||
// Create the CSR itself
|
if err != nil {
|
||||||
bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
|
t.Fatalf("error encoding CSR: %s", err)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating CSR: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error encoding CSR: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return csrBuf.String(), pkBuf.String()
|
return csrBuf.String(), pkPEM
|
||||||
}
|
}
|
||||||
|
|
||||||
// testKeyID returns a KeyID from the given public key. This just calls
|
// testKeyID returns a KeyID from the given public key. This just calls
|
||||||
|
@ -246,25 +233,26 @@ func testKeyID(t testing.T, raw interface{}) []byte {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// testMemoizePK is the private key that we memoize once we generate it
|
// testPrivateKey creates an ECDSA based private key. Both a crypto.Signer and
|
||||||
// once so that our tests don't rely on too much system entropy.
|
// the key in PEM form are returned.
|
||||||
var testMemoizePK atomic.Value
|
//
|
||||||
|
// NOTE(banks): this was memoized to save entropy during tests but it turns out
|
||||||
// testPrivateKey creates an ECDSA based private key.
|
// crypto/rand will never block and always reads from /dev/urandom on unix OSes
|
||||||
func testPrivateKey(t testing.T, ca *structs.CARoot) crypto.Signer {
|
// which does not consume entropy.
|
||||||
// If we already generated a private key, use that
|
//
|
||||||
var pk *ecdsa.PrivateKey
|
// If we find by profiling it's taking a lot of cycles we could optimise/cache
|
||||||
if v := testMemoizePK.Load(); v != nil {
|
// again but we at least need to use different keys for each distinct CA (when
|
||||||
pk = v.(*ecdsa.PrivateKey)
|
// multiple CAs are generated at once e.g. to test cross-signing) and a
|
||||||
}
|
// different one again for the leafs otherwise we risk tests that have false
|
||||||
|
// positives since signatures from different logical cert's keys are
|
||||||
// If we have no key, then create a new one.
|
// indistinguishable, but worse we build validation chains using AuthorityKeyID
|
||||||
if pk == nil {
|
// which will be the same for multiple CAs/Leafs. Also note that our UUID
|
||||||
var err error
|
// generator also reads from crypto rand and is called far more often during
|
||||||
pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
// tests than this will be.
|
||||||
if err != nil {
|
func testPrivateKey(t testing.T) (crypto.Signer, string) {
|
||||||
t.Fatalf("error generating private key: %s", err)
|
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatalf("error generating private key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := x509.MarshalECPrivateKey(pk)
|
bs, err := x509.MarshalECPrivateKey(pk)
|
||||||
|
@ -277,14 +265,8 @@ func testPrivateKey(t testing.T, ca *structs.CARoot) crypto.Signer {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error encoding private key: %s", err)
|
t.Fatalf("error encoding private key: %s", err)
|
||||||
}
|
}
|
||||||
if ca != nil {
|
|
||||||
ca.SigningKey = buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memoize the key
|
return pk, buf.String()
|
||||||
testMemoizePK.Store(pk)
|
|
||||||
|
|
||||||
return pk
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// testSerialNumber generates a serial number suitable for a certificate.
|
// testSerialNumber generates a serial number suitable for a certificate.
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestTestCAAndLeaf(t *testing.T) {
|
||||||
|
|
||||||
// Create the certs
|
// Create the certs
|
||||||
ca := TestCA(t, nil)
|
ca := TestCA(t, nil)
|
||||||
leaf := TestLeaf(t, "web", ca)
|
leaf, _ := TestLeaf(t, "web", ca)
|
||||||
|
|
||||||
// Create a temporary directory for storing the certs
|
// Create a temporary directory for storing the certs
|
||||||
td, err := ioutil.TempDir("", "consul")
|
td, err := ioutil.TempDir("", "consul")
|
||||||
|
@ -62,8 +62,8 @@ func TestTestCAAndLeaf_xc(t *testing.T) {
|
||||||
// Create the certs
|
// Create the certs
|
||||||
ca1 := TestCA(t, nil)
|
ca1 := TestCA(t, nil)
|
||||||
ca2 := TestCA(t, ca1)
|
ca2 := TestCA(t, ca1)
|
||||||
leaf1 := TestLeaf(t, "web", ca1)
|
leaf1, _ := TestLeaf(t, "web", ca1)
|
||||||
leaf2 := TestLeaf(t, "web", ca2)
|
leaf2, _ := TestLeaf(t, "web", ca2)
|
||||||
|
|
||||||
// Create a temporary directory for storing the certs
|
// Create a temporary directory for storing the certs
|
||||||
td, err := ioutil.TempDir("", "consul")
|
td, err := ioutil.TempDir("", "consul")
|
||||||
|
@ -98,12 +98,3 @@ func TestTestCAAndLeaf_xc(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that the private key is memoized to preseve system entropy.
|
|
||||||
func TestTestPrivateKey_memoize(t *testing.T) {
|
|
||||||
ca1 := TestCA(t, nil)
|
|
||||||
ca2 := TestCA(t, nil)
|
|
||||||
if ca1.SigningKey != ca2.SigningKey {
|
|
||||||
t.Fatal("should have the same signing keys for tests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package connect
|
|
||||||
|
|
||||||
import "crypto/x509"
|
|
||||||
|
|
||||||
// Auther is the interface that provides both Authentication and Authorization
|
|
||||||
// for an mTLS connection. It's only method is compatible with
|
|
||||||
// tls.Config.VerifyPeerCertificate.
|
|
||||||
type Auther interface {
|
|
||||||
// Auth is called during tls Connection establishment to Authenticate and
|
|
||||||
// Authorize the presented peer. Note that verifiedChains must not be relied
|
|
||||||
// upon as we typically have to skip Go's internal verification so the
|
|
||||||
// implementation takes full responsibility to validating the certificate
|
|
||||||
// against known roots. It is also up to the user of the interface to ensure
|
|
||||||
// appropriate validation is performed for client or server end by arranging
|
|
||||||
// for an appropriate implementation to be hooked into the tls.Config used.
|
|
||||||
Auth(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientAuther is used to auth Clients connecting to a Server.
|
|
||||||
type ClientAuther struct{}
|
|
||||||
|
|
||||||
// Auth implements Auther
|
|
||||||
func (a *ClientAuther) Auth(rawCerts [][]byte,
|
|
||||||
verifiedChains [][]*x509.Certificate) error {
|
|
||||||
|
|
||||||
// TODO(banks): implement path validation and AuthZ
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerAuther is used to auth the Server identify from a connecting Client.
|
|
||||||
type ServerAuther struct {
|
|
||||||
// TODO(banks): We'll need a way to pass the expected service identity (name,
|
|
||||||
// namespace, dc, cluster) here based on discovery result.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth implements Auther
|
|
||||||
func (a *ServerAuther) Auth(rawCerts [][]byte,
|
|
||||||
verifiedChains [][]*x509.Certificate) error {
|
|
||||||
|
|
||||||
// TODO(banks): implement path validation and verify URI matches the target
|
|
||||||
// service we intended to connect to.
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
// certgen: a tool for generating test certificates on disk for use as
|
||||||
|
// test-fixtures and for end-to-end testing and local development.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// $ go run connect/certgen/certgen.go -out-dir /tmp/connect-certs
|
||||||
|
//
|
||||||
|
// You can verify a given leaf with a given root using:
|
||||||
|
//
|
||||||
|
// $ openssl verify -verbose -CAfile ca2-ca.cert.pem ca1-svc-db.cert.pem
|
||||||
|
//
|
||||||
|
// Note that to verify via the cross-signed intermediate, openssl requires it to
|
||||||
|
// be bundled with the _root_ CA bundle and will ignore the cert if it's passed
|
||||||
|
// with the subject. You can do that with:
|
||||||
|
//
|
||||||
|
// $ openssl verify -verbose -CAfile \
|
||||||
|
// <(cat ca1-ca.cert.pem ca2-xc-by-ca1.cert.pem) \
|
||||||
|
// ca2-svc-db.cert.pem
|
||||||
|
// ca2-svc-db.cert.pem: OK
|
||||||
|
//
|
||||||
|
// Note that the same leaf and root without the intermediate should fail:
|
||||||
|
//
|
||||||
|
// $ openssl verify -verbose -CAfile ca1-ca.cert.pem ca2-svc-db.cert.pem
|
||||||
|
// ca2-svc-db.cert.pem: CN = db
|
||||||
|
// error 20 at 0 depth lookup:unable to get local issuer certificate
|
||||||
|
//
|
||||||
|
// NOTE: THIS IS A QUIRK OF OPENSSL; in Connect we distribute the roots alone
|
||||||
|
// and stable intermediates like the XC cert to the _leaf_.
|
||||||
|
package main // import "github.com/hashicorp/consul/connect/certgen"
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/mitchellh/go-testing-interface"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var numCAs = 2
|
||||||
|
var services = []string{"web", "db", "cache"}
|
||||||
|
//var slugRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
var outDir string
|
||||||
|
|
||||||
|
flag.StringVar(&outDir, "out-dir", "",
|
||||||
|
"REQUIRED: the dir to write certificates to")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if outDir == "" {
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CA certs
|
||||||
|
var prevCA *structs.CARoot
|
||||||
|
for i := 1; i <= numCAs; i++ {
|
||||||
|
ca := connect.TestCA(&testing.RuntimeT{}, prevCA)
|
||||||
|
prefix := fmt.Sprintf("%s/ca%d-ca", outDir, i)
|
||||||
|
writeFile(prefix+".cert.pem", ca.RootCert)
|
||||||
|
writeFile(prefix+".key.pem", ca.SigningKey)
|
||||||
|
if prevCA != nil {
|
||||||
|
fname := fmt.Sprintf("%s/ca%d-xc-by-ca%d.cert.pem", outDir, i, i-1)
|
||||||
|
writeFile(fname, ca.SigningCert)
|
||||||
|
}
|
||||||
|
prevCA = ca
|
||||||
|
|
||||||
|
// Create service certs for each CA
|
||||||
|
for _, svc := range services {
|
||||||
|
certPEM, keyPEM := connect.TestLeaf(&testing.RuntimeT{}, svc, ca)
|
||||||
|
prefix := fmt.Sprintf("%s/ca%d-svc-%s", outDir, i, svc)
|
||||||
|
writeFile(prefix+".cert.pem", certPEM)
|
||||||
|
writeFile(prefix+".key.pem", keyPEM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(name, content string) {
|
||||||
|
fmt.Println("Writing ", name)
|
||||||
|
err := ioutil.WriteFile(name, []byte(content), 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed writing file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,256 +1,256 @@
|
||||||
package connect
|
package connect
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"context"
|
// "context"
|
||||||
"crypto/tls"
|
// "crypto/tls"
|
||||||
"fmt"
|
// "fmt"
|
||||||
"math/rand"
|
// "math/rand"
|
||||||
"net"
|
// "net"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/api"
|
// "github.com/hashicorp/consul/api"
|
||||||
)
|
// )
|
||||||
|
|
||||||
// CertStatus indicates whether the Client currently has valid certificates for
|
// // CertStatus indicates whether the Client currently has valid certificates for
|
||||||
// incoming and outgoing connections.
|
// // incoming and outgoing connections.
|
||||||
type CertStatus int
|
// type CertStatus int
|
||||||
|
|
||||||
const (
|
// const (
|
||||||
// CertStatusUnknown is the zero value for CertStatus which may be returned
|
// // CertStatusUnknown is the zero value for CertStatus which may be returned
|
||||||
// when a watch channel is closed on shutdown. It has no other meaning.
|
// // when a watch channel is closed on shutdown. It has no other meaning.
|
||||||
CertStatusUnknown CertStatus = iota
|
// CertStatusUnknown CertStatus = iota
|
||||||
|
|
||||||
// CertStatusOK indicates the client has valid certificates and trust roots to
|
// // CertStatusOK indicates the client has valid certificates and trust roots to
|
||||||
// Authenticate incoming and outgoing connections.
|
// // Authenticate incoming and outgoing connections.
|
||||||
CertStatusOK
|
// CertStatusOK
|
||||||
|
|
||||||
// CertStatusPending indicates the client is waiting to be issued initial
|
// // CertStatusPending indicates the client is waiting to be issued initial
|
||||||
// certificates, or that it's certificates have expired and it's waiting to be
|
// // certificates, or that it's certificates have expired and it's waiting to be
|
||||||
// issued new ones. In this state all incoming and outgoing connections will
|
// // issued new ones. In this state all incoming and outgoing connections will
|
||||||
// fail.
|
// // fail.
|
||||||
CertStatusPending
|
// CertStatusPending
|
||||||
)
|
// )
|
||||||
|
|
||||||
func (s CertStatus) String() string {
|
// func (s CertStatus) String() string {
|
||||||
switch s {
|
// switch s {
|
||||||
case CertStatusOK:
|
// case CertStatusOK:
|
||||||
return "OK"
|
// return "OK"
|
||||||
case CertStatusPending:
|
// case CertStatusPending:
|
||||||
return "pending"
|
// return "pending"
|
||||||
case CertStatusUnknown:
|
// case CertStatusUnknown:
|
||||||
fallthrough
|
// fallthrough
|
||||||
default:
|
// default:
|
||||||
return "unknown"
|
// return "unknown"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Client is the interface a basic client implementation must support.
|
// // Client is the interface a basic client implementation must support.
|
||||||
type Client interface {
|
// type Client interface {
|
||||||
// TODO(banks): build this and test it
|
// // TODO(banks): build this and test it
|
||||||
// CertStatus returns the current status of the client's certificates. It can
|
// // CertStatus returns the current status of the client's certificates. It can
|
||||||
// be used to determine if the Client is able to service requests at the
|
// // be used to determine if the Client is able to service requests at the
|
||||||
// current time.
|
// // current time.
|
||||||
//CertStatus() CertStatus
|
// //CertStatus() CertStatus
|
||||||
|
|
||||||
// TODO(banks): build this and test it
|
// // TODO(banks): build this and test it
|
||||||
// WatchCertStatus returns a channel that is notified on all status changes.
|
// // WatchCertStatus returns a channel that is notified on all status changes.
|
||||||
// Note that a message on the channel isn't guaranteed to be different so it's
|
// // Note that a message on the channel isn't guaranteed to be different so it's
|
||||||
// value should be inspected. During Client shutdown the channel will be
|
// // value should be inspected. During Client shutdown the channel will be
|
||||||
// closed returning a zero type which is equivalent to CertStatusUnknown.
|
// // closed returning a zero type which is equivalent to CertStatusUnknown.
|
||||||
//WatchCertStatus() <-chan CertStatus
|
// //WatchCertStatus() <-chan CertStatus
|
||||||
|
|
||||||
// ServerTLSConfig returns the *tls.Config to be used when creating a TCP
|
// // ServerTLSConfig returns the *tls.Config to be used when creating a TCP
|
||||||
// listener that should accept Connect connections. It is likely that at
|
// // listener that should accept Connect connections. It is likely that at
|
||||||
// startup the tlsCfg returned will not be immediately usable since
|
// // startup the tlsCfg returned will not be immediately usable since
|
||||||
// certificates are typically fetched from the agent asynchronously. In this
|
// // certificates are typically fetched from the agent asynchronously. In this
|
||||||
// case it's still safe to listen with the provided config, but auth failures
|
// // case it's still safe to listen with the provided config, but auth failures
|
||||||
// will occur until initial certificate discovery is complete. In general at
|
// // will occur until initial certificate discovery is complete. In general at
|
||||||
// any time it is possible for certificates to expire before new replacements
|
// // any time it is possible for certificates to expire before new replacements
|
||||||
// have been issued due to local network errors so the server may not actually
|
// // have been issued due to local network errors so the server may not actually
|
||||||
// have a working certificate configuration at any time, however as soon as
|
// // have a working certificate configuration at any time, however as soon as
|
||||||
// valid certs can be issued it will automatically start working again so
|
// // valid certs can be issued it will automatically start working again so
|
||||||
// should take no action.
|
// // should take no action.
|
||||||
ServerTLSConfig() (*tls.Config, error)
|
// ServerTLSConfig() (*tls.Config, error)
|
||||||
|
|
||||||
// DialService opens a new connection to the named service registered in
|
// // DialService opens a new connection to the named service registered in
|
||||||
// Consul. It will perform service discovery to find healthy instances. If
|
// // Consul. It will perform service discovery to find healthy instances. If
|
||||||
// there is an error during connection it is returned and the caller may call
|
// // there is an error during connection it is returned and the caller may call
|
||||||
// again. The client implementation makes a best effort to make consecutive
|
// // again. The client implementation makes a best effort to make consecutive
|
||||||
// Dials against different instances either by randomising the list and/or
|
// // Dials against different instances either by randomising the list and/or
|
||||||
// maintaining a local memory of which instances recently failed. If the
|
// // maintaining a local memory of which instances recently failed. If the
|
||||||
// context passed times out before connection is established and verified an
|
// // context passed times out before connection is established and verified an
|
||||||
// error is returned.
|
// // error is returned.
|
||||||
DialService(ctx context.Context, namespace, name string) (net.Conn, error)
|
// DialService(ctx context.Context, namespace, name string) (net.Conn, error)
|
||||||
|
|
||||||
// DialPreparedQuery opens a new connection by executing the named Prepared
|
// // DialPreparedQuery opens a new connection by executing the named Prepared
|
||||||
// Query against the local Consul agent, and picking one of the returned
|
// // Query against the local Consul agent, and picking one of the returned
|
||||||
// instances to connect to. It will perform service discovery with the same
|
// // instances to connect to. It will perform service discovery with the same
|
||||||
// semantics as DialService.
|
// // semantics as DialService.
|
||||||
DialPreparedQuery(ctx context.Context, namespace, name string) (net.Conn, error)
|
// DialPreparedQuery(ctx context.Context, namespace, name string) (net.Conn, error)
|
||||||
}
|
// }
|
||||||
|
|
||||||
/*
|
// /*
|
||||||
|
|
||||||
Maybe also convenience wrappers for:
|
// Maybe also convenience wrappers for:
|
||||||
- listening TLS conn with right config
|
// - listening TLS conn with right config
|
||||||
- http.ListenAndServeTLS equivalent
|
// - http.ListenAndServeTLS equivalent
|
||||||
|
|
||||||
*/
|
// */
|
||||||
|
|
||||||
// AgentClient is the primary implementation of a connect.Client which
|
// // AgentClient is the primary implementation of a connect.Client which
|
||||||
// communicates with the local Consul agent.
|
// // communicates with the local Consul agent.
|
||||||
type AgentClient struct {
|
// type AgentClient struct {
|
||||||
agent *api.Client
|
// agent *api.Client
|
||||||
tlsCfg *ReloadableTLSConfig
|
// tlsCfg *ReloadableTLSConfig
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewClient returns an AgentClient to allow consuming and providing
|
// // NewClient returns an AgentClient to allow consuming and providing
|
||||||
// Connect-enabled network services.
|
// // Connect-enabled network services.
|
||||||
func NewClient(agent *api.Client) Client {
|
// func NewClient(agent *api.Client) Client {
|
||||||
// TODO(banks): hook up fetching certs from Agent and updating tlsCfg on cert
|
// // TODO(banks): hook up fetching certs from Agent and updating tlsCfg on cert
|
||||||
// delivery/change. Perhaps need to make
|
// // delivery/change. Perhaps need to make
|
||||||
return &AgentClient{
|
// return &AgentClient{
|
||||||
agent: agent,
|
// agent: agent,
|
||||||
tlsCfg: NewReloadableTLSConfig(defaultTLSConfig()),
|
// tlsCfg: NewReloadableTLSConfig(defaultTLSConfig()),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewInsecureDevClientWithLocalCerts returns an AgentClient that will still do
|
// // NewInsecureDevClientWithLocalCerts returns an AgentClient that will still do
|
||||||
// service discovery via the local agent but will use externally provided
|
// // service discovery via the local agent but will use externally provided
|
||||||
// certificates and skip authorization. This is intended just for development
|
// // certificates and skip authorization. This is intended just for development
|
||||||
// and must not be used ever in production.
|
// // and must not be used ever in production.
|
||||||
func NewInsecureDevClientWithLocalCerts(agent *api.Client, caFile, certFile,
|
// func NewInsecureDevClientWithLocalCerts(agent *api.Client, caFile, certFile,
|
||||||
keyFile string) (Client, error) {
|
// keyFile string) (Client, error) {
|
||||||
|
|
||||||
cfg, err := devTLSConfigFromFiles(caFile, certFile, keyFile)
|
// cfg, err := devTLSConfigFromFiles(caFile, certFile, keyFile)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return &AgentClient{
|
// return &AgentClient{
|
||||||
agent: agent,
|
// agent: agent,
|
||||||
tlsCfg: NewReloadableTLSConfig(cfg),
|
// tlsCfg: NewReloadableTLSConfig(cfg),
|
||||||
}, nil
|
// }, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ServerTLSConfig implements Client
|
// // ServerTLSConfig implements Client
|
||||||
func (c *AgentClient) ServerTLSConfig() (*tls.Config, error) {
|
// func (c *AgentClient) ServerTLSConfig() (*tls.Config, error) {
|
||||||
return c.tlsCfg.ServerTLSConfig(), nil
|
// return c.tlsCfg.ServerTLSConfig(), nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// DialService implements Client
|
// // DialService implements Client
|
||||||
func (c *AgentClient) DialService(ctx context.Context, namespace,
|
// func (c *AgentClient) DialService(ctx context.Context, namespace,
|
||||||
name string) (net.Conn, error) {
|
// name string) (net.Conn, error) {
|
||||||
return c.dial(ctx, "service", namespace, name)
|
// return c.dial(ctx, "service", namespace, name)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// DialPreparedQuery implements Client
|
// // DialPreparedQuery implements Client
|
||||||
func (c *AgentClient) DialPreparedQuery(ctx context.Context, namespace,
|
// func (c *AgentClient) DialPreparedQuery(ctx context.Context, namespace,
|
||||||
name string) (net.Conn, error) {
|
// name string) (net.Conn, error) {
|
||||||
return c.dial(ctx, "prepared_query", namespace, name)
|
// return c.dial(ctx, "prepared_query", namespace, name)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (c *AgentClient) dial(ctx context.Context, discoveryType, namespace,
|
// func (c *AgentClient) dial(ctx context.Context, discoveryType, namespace,
|
||||||
name string) (net.Conn, error) {
|
// name string) (net.Conn, error) {
|
||||||
|
|
||||||
svcs, err := c.discoverInstances(ctx, discoveryType, namespace, name)
|
// svcs, err := c.discoverInstances(ctx, discoveryType, namespace, name)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
svc, err := c.pickInstance(svcs)
|
// svc, err := c.pickInstance(svcs)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
if svc == nil {
|
// if svc == nil {
|
||||||
return nil, fmt.Errorf("no healthy services discovered")
|
// return nil, fmt.Errorf("no healthy services discovered")
|
||||||
}
|
// }
|
||||||
|
|
||||||
// OK we have a service we can dial! We need a ClientAuther that will validate
|
// // OK we have a service we can dial! We need a ClientAuther that will validate
|
||||||
// the connection is legit.
|
// // the connection is legit.
|
||||||
|
|
||||||
// TODO(banks): implement ClientAuther properly to actually verify connected
|
// // TODO(banks): implement ClientAuther properly to actually verify connected
|
||||||
// cert matches the expected service/cluster etc. based on svc.
|
// // cert matches the expected service/cluster etc. based on svc.
|
||||||
auther := &ClientAuther{}
|
// auther := &ClientAuther{}
|
||||||
tlsConfig := c.tlsCfg.TLSConfig(auther)
|
// tlsConfig := c.tlsCfg.TLSConfig(auther)
|
||||||
|
|
||||||
// Resolve address TODO(banks): I expected this to happen magically in the
|
// // Resolve address TODO(banks): I expected this to happen magically in the
|
||||||
// agent at registration time if I register with no explicit address but
|
// // agent at registration time if I register with no explicit address but
|
||||||
// apparently doesn't. This is a quick hack to make it work for now, need to
|
// // apparently doesn't. This is a quick hack to make it work for now, need to
|
||||||
// see if there is a better shared code path for doing this.
|
// // see if there is a better shared code path for doing this.
|
||||||
addr := svc.Service.Address
|
// addr := svc.Service.Address
|
||||||
if addr == "" {
|
// if addr == "" {
|
||||||
addr = svc.Node.Address
|
// addr = svc.Node.Address
|
||||||
}
|
// }
|
||||||
var dialer net.Dialer
|
// var dialer net.Dialer
|
||||||
tcpConn, err := dialer.DialContext(ctx, "tcp",
|
// tcpConn, err := dialer.DialContext(ctx, "tcp",
|
||||||
fmt.Sprintf("%s:%d", addr, svc.Service.Port))
|
// fmt.Sprintf("%s:%d", addr, svc.Service.Port))
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
tlsConn := tls.Client(tcpConn, tlsConfig)
|
// tlsConn := tls.Client(tcpConn, tlsConfig)
|
||||||
err = tlsConn.Handshake()
|
// err = tlsConn.Handshake()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
tlsConn.Close()
|
// tlsConn.Close()
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return tlsConn, nil
|
// return tlsConn, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// pickInstance returns an instance from the given list to try to connect to. It
|
// // pickInstance returns an instance from the given list to try to connect to. It
|
||||||
// may be made pluggable later, for now it just picks a random one regardless of
|
// // may be made pluggable later, for now it just picks a random one regardless of
|
||||||
// whether the list is already shuffled.
|
// // whether the list is already shuffled.
|
||||||
func (c *AgentClient) pickInstance(svcs []*api.ServiceEntry) (*api.ServiceEntry, error) {
|
// func (c *AgentClient) pickInstance(svcs []*api.ServiceEntry) (*api.ServiceEntry, error) {
|
||||||
if len(svcs) < 1 {
|
// if len(svcs) < 1 {
|
||||||
return nil, nil
|
// return nil, nil
|
||||||
}
|
// }
|
||||||
idx := rand.Intn(len(svcs))
|
// idx := rand.Intn(len(svcs))
|
||||||
return svcs[idx], nil
|
// return svcs[idx], nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// discoverInstances returns all instances for the given discoveryType,
|
// // discoverInstances returns all instances for the given discoveryType,
|
||||||
// namespace and name. The returned service entries may or may not be shuffled
|
// // namespace and name. The returned service entries may or may not be shuffled
|
||||||
func (c *AgentClient) discoverInstances(ctx context.Context, discoverType,
|
// func (c *AgentClient) discoverInstances(ctx context.Context, discoverType,
|
||||||
namespace, name string) ([]*api.ServiceEntry, error) {
|
// namespace, name string) ([]*api.ServiceEntry, error) {
|
||||||
|
|
||||||
q := &api.QueryOptions{
|
// q := &api.QueryOptions{
|
||||||
// TODO(banks): make this configurable?
|
// // TODO(banks): make this configurable?
|
||||||
AllowStale: true,
|
// AllowStale: true,
|
||||||
}
|
// }
|
||||||
q = q.WithContext(ctx)
|
// q = q.WithContext(ctx)
|
||||||
|
|
||||||
switch discoverType {
|
// switch discoverType {
|
||||||
case "service":
|
// case "service":
|
||||||
svcs, _, err := c.agent.Health().Connect(name, "", true, q)
|
// svcs, _, err := c.agent.Health().Connect(name, "", true, q)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
return svcs, err
|
// return svcs, err
|
||||||
|
|
||||||
case "prepared_query":
|
// case "prepared_query":
|
||||||
// TODO(banks): it's not super clear to me how this should work eventually.
|
// // TODO(banks): it's not super clear to me how this should work eventually.
|
||||||
// How do we distinguise between a PreparedQuery for the actual services and
|
// // How do we distinguise between a PreparedQuery for the actual services and
|
||||||
// one that should return the connect proxies where that differs? If we
|
// // one that should return the connect proxies where that differs? If we
|
||||||
// can't then we end up with a janky UX where user specifies a reasonable
|
// // can't then we end up with a janky UX where user specifies a reasonable
|
||||||
// prepared query but we try to connect to non-connect services and fail
|
// // prepared query but we try to connect to non-connect services and fail
|
||||||
// with a confusing TLS error. Maybe just a way to filter PreparedQuery
|
// // with a confusing TLS error. Maybe just a way to filter PreparedQuery
|
||||||
// results by connect-enabled would be sufficient (or even metadata to do
|
// // results by connect-enabled would be sufficient (or even metadata to do
|
||||||
// that ourselves in the response although less efficient).
|
// // that ourselves in the response although less efficient).
|
||||||
resp, _, err := c.agent.PreparedQuery().Execute(name, q)
|
// resp, _, err := c.agent.PreparedQuery().Execute(name, q)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Awkward, we have a slice of api.ServiceEntry here but want a slice of
|
// // Awkward, we have a slice of api.ServiceEntry here but want a slice of
|
||||||
// *api.ServiceEntry for compat with Connect/Service APIs. Have to convert
|
// // *api.ServiceEntry for compat with Connect/Service APIs. Have to convert
|
||||||
// them to keep things type-happy.
|
// // them to keep things type-happy.
|
||||||
svcs := make([]*api.ServiceEntry, len(resp.Nodes))
|
// svcs := make([]*api.ServiceEntry, len(resp.Nodes))
|
||||||
for idx, se := range resp.Nodes {
|
// for idx, se := range resp.Nodes {
|
||||||
svcs[idx] = &se
|
// svcs[idx] = &se
|
||||||
}
|
// }
|
||||||
return svcs, err
|
// return svcs, err
|
||||||
default:
|
// default:
|
||||||
return nil, fmt.Errorf("unsupported discovery type: %s", discoverType)
|
// return nil, fmt.Errorf("unsupported discovery type: %s", discoverType)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
@ -1,148 +1,148 @@
|
||||||
package connect
|
package connect
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"context"
|
// "context"
|
||||||
"crypto/x509"
|
// "crypto/x509"
|
||||||
"crypto/x509/pkix"
|
// "crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
// "encoding/asn1"
|
||||||
"io/ioutil"
|
// "io/ioutil"
|
||||||
"net"
|
// "net"
|
||||||
"net/http"
|
// "net/http"
|
||||||
"net/http/httptest"
|
// "net/http/httptest"
|
||||||
"net/url"
|
// "net/url"
|
||||||
"strconv"
|
// "strconv"
|
||||||
"testing"
|
// "testing"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/api"
|
// "github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/testutil"
|
// "github.com/hashicorp/consul/testutil"
|
||||||
"github.com/stretchr/testify/require"
|
// "github.com/stretchr/testify/require"
|
||||||
)
|
// )
|
||||||
|
|
||||||
func TestNewInsecureDevClientWithLocalCerts(t *testing.T) {
|
// func TestNewInsecureDevClientWithLocalCerts(t *testing.T) {
|
||||||
|
|
||||||
agent, err := api.NewClient(api.DefaultConfig())
|
// agent, err := api.NewClient(api.DefaultConfig())
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
got, err := NewInsecureDevClientWithLocalCerts(agent,
|
// got, err := NewInsecureDevClientWithLocalCerts(agent,
|
||||||
"testdata/ca1-ca-consul-internal.cert.pem",
|
// "testdata/ca1-ca-consul-internal.cert.pem",
|
||||||
"testdata/ca1-svc-web.cert.pem",
|
// "testdata/ca1-svc-web.cert.pem",
|
||||||
"testdata/ca1-svc-web.key.pem",
|
// "testdata/ca1-svc-web.key.pem",
|
||||||
)
|
// )
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
// Sanity check correct certs were loaded
|
// // Sanity check correct certs were loaded
|
||||||
serverCfg, err := got.ServerTLSConfig()
|
// serverCfg, err := got.ServerTLSConfig()
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
caSubjects := serverCfg.RootCAs.Subjects()
|
// caSubjects := serverCfg.RootCAs.Subjects()
|
||||||
require.Len(t, caSubjects, 1)
|
// require.Len(t, caSubjects, 1)
|
||||||
caSubject, err := testNameFromRawDN(caSubjects[0])
|
// caSubject, err := testNameFromRawDN(caSubjects[0])
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
require.Equal(t, "Consul Internal", caSubject.CommonName)
|
// require.Equal(t, "Consul Internal", caSubject.CommonName)
|
||||||
|
|
||||||
require.Len(t, serverCfg.Certificates, 1)
|
// require.Len(t, serverCfg.Certificates, 1)
|
||||||
cert, err := x509.ParseCertificate(serverCfg.Certificates[0].Certificate[0])
|
// cert, err := x509.ParseCertificate(serverCfg.Certificates[0].Certificate[0])
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
require.Equal(t, "web", cert.Subject.CommonName)
|
// require.Equal(t, "web", cert.Subject.CommonName)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func testNameFromRawDN(raw []byte) (*pkix.Name, error) {
|
// func testNameFromRawDN(raw []byte) (*pkix.Name, error) {
|
||||||
var seq pkix.RDNSequence
|
// var seq pkix.RDNSequence
|
||||||
if _, err := asn1.Unmarshal(raw, &seq); err != nil {
|
// if _, err := asn1.Unmarshal(raw, &seq); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
var name pkix.Name
|
// var name pkix.Name
|
||||||
name.FillFromRDNSequence(&seq)
|
// name.FillFromRDNSequence(&seq)
|
||||||
return &name, nil
|
// return &name, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func testAgent(t *testing.T) (*testutil.TestServer, *api.Client) {
|
// func testAgent(t *testing.T) (*testutil.TestServer, *api.Client) {
|
||||||
t.Helper()
|
// t.Helper()
|
||||||
|
|
||||||
// Make client config
|
// // Make client config
|
||||||
conf := api.DefaultConfig()
|
// conf := api.DefaultConfig()
|
||||||
|
|
||||||
// Create server
|
// // Create server
|
||||||
server, err := testutil.NewTestServerConfigT(t, nil)
|
// server, err := testutil.NewTestServerConfigT(t, nil)
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
conf.Address = server.HTTPAddr
|
// conf.Address = server.HTTPAddr
|
||||||
|
|
||||||
// Create client
|
// // Create client
|
||||||
agent, err := api.NewClient(conf)
|
// agent, err := api.NewClient(conf)
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
return server, agent
|
// return server, agent
|
||||||
}
|
// }
|
||||||
|
|
||||||
func testService(t *testing.T, ca, name string, client *api.Client) *httptest.Server {
|
// func testService(t *testing.T, ca, name string, client *api.Client) *httptest.Server {
|
||||||
t.Helper()
|
// t.Helper()
|
||||||
|
|
||||||
// Run a test service to discover
|
// // Run a test service to discover
|
||||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
// server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("svc: " + name))
|
// w.Write([]byte("svc: " + name))
|
||||||
}))
|
// }))
|
||||||
server.TLS = TestTLSConfig(t, ca, name)
|
// server.TLS = TestTLSConfig(t, ca, name)
|
||||||
server.StartTLS()
|
// server.StartTLS()
|
||||||
|
|
||||||
u, err := url.Parse(server.URL)
|
// u, err := url.Parse(server.URL)
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
port, err := strconv.Atoi(u.Port())
|
// port, err := strconv.Atoi(u.Port())
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
// If client is passed, register the test service instance
|
// // If client is passed, register the test service instance
|
||||||
if client != nil {
|
// if client != nil {
|
||||||
svc := &api.AgentServiceRegistration{
|
// svc := &api.AgentServiceRegistration{
|
||||||
// TODO(banks): we don't really have a good way to represent
|
// // TODO(banks): we don't really have a good way to represent
|
||||||
// connect-native apps yet so we have to pretend out little server is a
|
// // connect-native apps yet so we have to pretend out little server is a
|
||||||
// proxy for now.
|
// // proxy for now.
|
||||||
Kind: api.ServiceKindConnectProxy,
|
// Kind: api.ServiceKindConnectProxy,
|
||||||
ProxyDestination: name,
|
// ProxyDestination: name,
|
||||||
Name: name + "-proxy",
|
// Name: name + "-proxy",
|
||||||
Address: u.Hostname(),
|
// Address: u.Hostname(),
|
||||||
Port: port,
|
// Port: port,
|
||||||
}
|
// }
|
||||||
err := client.Agent().ServiceRegister(svc)
|
// err := client.Agent().ServiceRegister(svc)
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
return server
|
// return server
|
||||||
}
|
// }
|
||||||
|
|
||||||
func TestDialService(t *testing.T) {
|
// func TestDialService(t *testing.T) {
|
||||||
consulServer, agent := testAgent(t)
|
// consulServer, agent := testAgent(t)
|
||||||
defer consulServer.Stop()
|
// defer consulServer.Stop()
|
||||||
|
|
||||||
svc := testService(t, "ca1", "web", agent)
|
// svc := testService(t, "ca1", "web", agent)
|
||||||
defer svc.Close()
|
// defer svc.Close()
|
||||||
|
|
||||||
c, err := NewInsecureDevClientWithLocalCerts(agent,
|
// c, err := NewInsecureDevClientWithLocalCerts(agent,
|
||||||
"testdata/ca1-ca-consul-internal.cert.pem",
|
// "testdata/ca1-ca-consul-internal.cert.pem",
|
||||||
"testdata/ca1-svc-web.cert.pem",
|
// "testdata/ca1-svc-web.cert.pem",
|
||||||
"testdata/ca1-svc-web.key.pem",
|
// "testdata/ca1-svc-web.key.pem",
|
||||||
)
|
// )
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
conn, err := c.DialService(context.Background(), "default", "web")
|
// conn, err := c.DialService(context.Background(), "default", "web")
|
||||||
require.Nilf(t, err, "err: %s", err)
|
// require.Nilf(t, err, "err: %s", err)
|
||||||
|
|
||||||
// Inject our conn into http.Transport
|
// // Inject our conn into http.Transport
|
||||||
httpClient := &http.Client{
|
// httpClient := &http.Client{
|
||||||
Transport: &http.Transport{
|
// Transport: &http.Transport{
|
||||||
DialTLS: func(network, addr string) (net.Conn, error) {
|
// DialTLS: func(network, addr string) (net.Conn, error) {
|
||||||
return conn, nil
|
// return conn, nil
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Don't be fooled the hostname here is ignored since we did the dialling
|
// // Don't be fooled the hostname here is ignored since we did the dialling
|
||||||
// ourselves
|
// // ourselves
|
||||||
resp, err := httpClient.Get("https://web.connect.consul/")
|
// resp, err := httpClient.Get("https://web.connect.consul/")
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
defer resp.Body.Close()
|
// defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
// body, err := ioutil.ReadAll(resp.Body)
|
||||||
require.Nil(t, err)
|
// require.Nil(t, err)
|
||||||
|
|
||||||
require.Equal(t, "svc: web", string(body))
|
// require.Equal(t, "svc: web", string(body))
|
||||||
}
|
// }
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiHandler struct{}
|
||||||
|
|
||||||
|
func (apiHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}
|
||||||
|
|
||||||
|
// Note: this assumes a suitable Consul ACL token with 'service:write' for
|
||||||
|
// service 'web' is set in CONSUL_HTTP_TOKEN ENV var.
|
||||||
|
func ExampleService_ServerTLSConfig_hTTP() {
|
||||||
|
client, _ := api.NewClient(api.DefaultConfig())
|
||||||
|
svc, _ := NewService("web", client)
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: apiHandler{},
|
||||||
|
TLSConfig: svc.ServerTLSConfig(),
|
||||||
|
}
|
||||||
|
// Cert and key files are blank since the tls.Config will handle providing
|
||||||
|
// those dynamically.
|
||||||
|
log.Fatal(server.ListenAndServeTLS("", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptLoop(l net.Listener) {}
|
||||||
|
|
||||||
|
// Note: this assumes a suitable Consul ACL token with 'service:write' for
|
||||||
|
// service 'web' is set in CONSUL_HTTP_TOKEN ENV var.
|
||||||
|
func ExampleService_ServerTLSConfig_tLS() {
|
||||||
|
client, _ := api.NewClient(api.DefaultConfig())
|
||||||
|
svc, _ := NewService("web", client)
|
||||||
|
l, _ := tls.Listen("tcp", ":8080", svc.ServerTLSConfig())
|
||||||
|
acceptLoop(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleResponse(r *http.Response) {}
|
||||||
|
|
||||||
|
// Note: this assumes a suitable Consul ACL token with 'service:write' for
|
||||||
|
// service 'web' is set in CONSUL_HTTP_TOKEN ENV var.
|
||||||
|
func ExampleService_HTTPClient() {
|
||||||
|
client, _ := api.NewClient(api.DefaultConfig())
|
||||||
|
svc, _ := NewService("web", client)
|
||||||
|
|
||||||
|
httpClient := svc.HTTPClient()
|
||||||
|
resp, _ := httpClient.Get("https://web.service.consul/foo/bar")
|
||||||
|
handleResponse(resp)
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
testing "github.com/mitchellh/go-testing-interface"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver is the interface implemented by a service discovery mechanism.
|
||||||
|
type Resolver interface {
|
||||||
|
// Resolve returns a single service instance to connect to. Implementations
|
||||||
|
// may attempt to ensure the instance returned is currently available. It is
|
||||||
|
// expected that a client will re-dial on a connection failure so making an
|
||||||
|
// effort to return a different service instance each time where available
|
||||||
|
// increases reliability. The context passed can be used to impose timeouts
|
||||||
|
// which may or may not be respected by implementations that make network
|
||||||
|
// calls to resolve the service. The addr returned is a string in any valid
|
||||||
|
// form for passing directly to `net.Dial("tcp", addr)`.
|
||||||
|
Resolve(ctx context.Context) (addr string, certURI connect.CertURI, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticResolver is a statically defined resolver. This can be used to connect
|
||||||
|
// to an known-Connect endpoint without performing service discovery.
|
||||||
|
type StaticResolver struct {
|
||||||
|
// Addr is the network address (including port) of the instance. It must be
|
||||||
|
// the connect-enabled mTLS server and may be a proxy in front of the actual
|
||||||
|
// target service process. It is a string in any valid form for passing
|
||||||
|
// directly to `net.Dial("tcp", addr)`.
|
||||||
|
Addr string
|
||||||
|
|
||||||
|
// CertURL is the _identity_ we expect the server to present in it's TLS
|
||||||
|
// certificate. It must be an exact match or the connection will be rejected.
|
||||||
|
CertURI connect.CertURI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve implements Resolver by returning the static values.
|
||||||
|
func (sr *StaticResolver) Resolve(ctx context.Context) (string, connect.CertURI, error) {
|
||||||
|
return sr.Addr, sr.CertURI, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ConsulResolverTypeService indicates resolving healthy service nodes.
|
||||||
|
ConsulResolverTypeService int = iota
|
||||||
|
|
||||||
|
// ConsulResolverTypePreparedQuery indicates resolving via prepared query.
|
||||||
|
ConsulResolverTypePreparedQuery
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConsulResolver queries Consul for a service instance.
|
||||||
|
type ConsulResolver struct {
|
||||||
|
// Client is the Consul API client to use. Must be non-nil or Resolve will
|
||||||
|
// panic.
|
||||||
|
Client *api.Client
|
||||||
|
|
||||||
|
// Namespace of the query target
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
// Name of the query target
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Type of the query target,
|
||||||
|
Type int
|
||||||
|
|
||||||
|
// Datacenter to resolve in, empty indicates agent's local DC.
|
||||||
|
Datacenter string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve performs service discovery against the local Consul agent and returns
|
||||||
|
// the address and expected identity of a suitable service instance.
|
||||||
|
func (cr *ConsulResolver) Resolve(ctx context.Context) (string, connect.CertURI, error) {
|
||||||
|
switch cr.Type {
|
||||||
|
case ConsulResolverTypeService:
|
||||||
|
return cr.resolveService(ctx)
|
||||||
|
case ConsulResolverTypePreparedQuery:
|
||||||
|
// TODO(banks): we need to figure out what API changes are needed for
|
||||||
|
// prepared queries to become connect-aware. How do we signal that we want
|
||||||
|
// connect-enabled endpoints vs the direct ones for the responses?
|
||||||
|
return "", nil, fmt.Errorf("not implemented")
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unknown resolver type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ConsulResolver) resolveService(ctx context.Context) (string, connect.CertURI, error) {
|
||||||
|
health := cr.Client.Health()
|
||||||
|
|
||||||
|
svcs, _, err := health.Connect(cr.Name, "", true, cr.queryOptions(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(svcs) < 1 {
|
||||||
|
return "", nil, fmt.Errorf("no healthy instances found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services are not shuffled by HTTP API, pick one at (pseudo) random.
|
||||||
|
idx := 0
|
||||||
|
if len(svcs) > 1 {
|
||||||
|
idx = rand.Intn(len(svcs))
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := svcs[idx].Service.Address
|
||||||
|
if addr == "" {
|
||||||
|
addr = svcs[idx].Node.Address
|
||||||
|
}
|
||||||
|
port := svcs[idx].Service.Port
|
||||||
|
|
||||||
|
// Generate the expected CertURI
|
||||||
|
|
||||||
|
// TODO(banks): when we've figured out the CA story around generating and
|
||||||
|
// propagating these trust domains we need to actually fetch the trust domain
|
||||||
|
// somehow. We also need to implement namespaces. Use of test function here is
|
||||||
|
// temporary pending the work on trust domains.
|
||||||
|
certURI := connect.TestSpiffeIDService(&testing.RuntimeT{}, cr.Name)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s:%d", addr, port), certURI, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ConsulResolver) queryOptions(ctx context.Context) *api.QueryOptions {
|
||||||
|
q := &api.QueryOptions{
|
||||||
|
// We may make this configurable one day but we may also implement our own
|
||||||
|
// caching which is even more stale so...
|
||||||
|
AllowStale: true,
|
||||||
|
Datacenter: cr.Datacenter,
|
||||||
|
}
|
||||||
|
return q.WithContext(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent"
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStaticResolver_Resolve(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Addr string
|
||||||
|
CertURI connect.CertURI
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simples",
|
||||||
|
fields: fields{"1.2.3.4:80", connect.TestSpiffeIDService(t, "foo")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sr := StaticResolver{
|
||||||
|
Addr: tt.fields.Addr,
|
||||||
|
CertURI: tt.fields.CertURI,
|
||||||
|
}
|
||||||
|
addr, certURI, err := sr.Resolve(context.Background())
|
||||||
|
require := require.New(t)
|
||||||
|
require.Nil(err)
|
||||||
|
require.Equal(sr.Addr, addr)
|
||||||
|
require.Equal(sr.CertURI, certURI)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsulResolver_Resolve(t *testing.T) {
|
||||||
|
|
||||||
|
// Setup a local test agent to query
|
||||||
|
agent := agent.NewTestAgent("test-consul", "")
|
||||||
|
defer agent.Shutdown()
|
||||||
|
|
||||||
|
cfg := api.DefaultConfig()
|
||||||
|
cfg.Address = agent.HTTPAddr()
|
||||||
|
client, err := api.NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Setup a service with a connect proxy instance
|
||||||
|
regSrv := &api.AgentServiceRegistration{
|
||||||
|
Name: "web",
|
||||||
|
Port: 8080,
|
||||||
|
}
|
||||||
|
err = client.Agent().ServiceRegister(regSrv)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
regProxy := &api.AgentServiceRegistration{
|
||||||
|
Kind: "connect-proxy",
|
||||||
|
Name: "web-proxy",
|
||||||
|
Port: 9090,
|
||||||
|
ProxyDestination: "web",
|
||||||
|
}
|
||||||
|
err = client.Agent().ServiceRegister(regProxy)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// And another proxy so we can test handling with multiple endpoints returned
|
||||||
|
regProxy.Port = 9091
|
||||||
|
regProxy.ID = "web-proxy-2"
|
||||||
|
err = client.Agent().ServiceRegister(regProxy)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
proxyAddrs := []string{
|
||||||
|
agent.Config.AdvertiseAddrLAN.String() + ":9090",
|
||||||
|
agent.Config.AdvertiseAddrLAN.String() + ":9091",
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
Type int
|
||||||
|
Datacenter string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
timeout time.Duration
|
||||||
|
wantAddr string
|
||||||
|
wantCertURI connect.CertURI
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic service discovery",
|
||||||
|
fields: fields{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "web",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
},
|
||||||
|
wantCertURI: connect.TestSpiffeIDService(t, "web"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bad Type errors",
|
||||||
|
fields: fields{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "web",
|
||||||
|
Type: 123,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existent service errors",
|
||||||
|
fields: fields{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "timeout errors",
|
||||||
|
fields: fields{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "web",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
},
|
||||||
|
timeout: 1 * time.Nanosecond,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
cr := &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Namespace: tt.fields.Namespace,
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
Type: tt.fields.Type,
|
||||||
|
Datacenter: tt.fields.Datacenter,
|
||||||
|
}
|
||||||
|
// WithCancel just to have a cancel func in scope to assign in the if
|
||||||
|
// clause.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
if tt.timeout > 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, tt.timeout)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
gotAddr, gotCertURI, err := cr.Resolve(ctx)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.NotNil(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Nil(err)
|
||||||
|
// Address should be either of the registered proxy ports so check both
|
||||||
|
require.Contains(proxyAddrs, gotAddr)
|
||||||
|
require.Equal(tt.wantCertURI, gotCertURI)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
package connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a Consul service that accepts and/or connects via Connect.
|
||||||
|
// This can represent a service that only is a server, only is a client, or
|
||||||
|
// both.
|
||||||
|
//
|
||||||
|
// TODO(banks): API for monitoring status of certs from app
|
||||||
|
//
|
||||||
|
// TODO(banks): Agent implicit health checks based on knowing which certs are
|
||||||
|
// available should prevent clients being routed until the agent knows the
|
||||||
|
// service has been delivered valid certificates. Once built, document that here
|
||||||
|
// too.
|
||||||
|
type Service struct {
|
||||||
|
// serviceID is the unique ID for this service in the agent-local catalog.
|
||||||
|
// This is often but not always the service name. This is used to request
|
||||||
|
// Connect metadata. If the service with this ID doesn't exist on the local
|
||||||
|
// agent no error will be returned and the Service will retry periodically.
|
||||||
|
// This allows service startup and registration to happen in either order
|
||||||
|
// without coordination since they might be performed by separate processes.
|
||||||
|
serviceID string
|
||||||
|
|
||||||
|
// client is the Consul API client. It must be configured with an appropriate
|
||||||
|
// Token that has `service:write` policy on the provided ServiceID. If an
|
||||||
|
// insufficient token is provided, the Service will abort further attempts to
|
||||||
|
// fetch certificates and print a loud error message. It will not Close() or
|
||||||
|
// kill the process since that could lead to a crash loop in every service if
|
||||||
|
// ACL token was revoked. All attempts to dial will error and any incoming
|
||||||
|
// connections will fail to verify.
|
||||||
|
client *api.Client
|
||||||
|
|
||||||
|
// serverTLSCfg is the (reloadable) TLS config we use for serving.
|
||||||
|
serverTLSCfg *ReloadableTLSConfig
|
||||||
|
|
||||||
|
// clientTLSCfg is the (reloadable) TLS config we use for dialling.
|
||||||
|
clientTLSCfg *ReloadableTLSConfig
|
||||||
|
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates and starts a Service. The caller must close the returned
|
||||||
|
// service to free resources and allow the program to exit normally. This is
|
||||||
|
// typically called in a signal handler.
|
||||||
|
func NewService(serviceID string, client *api.Client) (*Service, error) {
|
||||||
|
return NewServiceWithLogger(serviceID, client,
|
||||||
|
log.New(os.Stderr, "", log.LstdFlags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServiceWithLogger starts the service with a specified log.Logger.
|
||||||
|
func NewServiceWithLogger(serviceID string, client *api.Client,
|
||||||
|
logger *log.Logger) (*Service, error) {
|
||||||
|
s := &Service{
|
||||||
|
serviceID: serviceID,
|
||||||
|
client: client,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
s.serverTLSCfg = NewReloadableTLSConfig(defaultTLSConfig(serverVerifyCerts))
|
||||||
|
s.clientTLSCfg = NewReloadableTLSConfig(defaultTLSConfig(clientVerifyCerts))
|
||||||
|
|
||||||
|
// TODO(banks) run the background certificate sync
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDevServiceFromCertFiles creates a Service using certificate and key files
|
||||||
|
// passed instead of fetching them from the client.
|
||||||
|
func NewDevServiceFromCertFiles(serviceID string, client *api.Client,
|
||||||
|
logger *log.Logger, caFile, certFile, keyFile string) (*Service, error) {
|
||||||
|
s := &Service{
|
||||||
|
serviceID: serviceID,
|
||||||
|
client: client,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
tlsCfg, err := devTLSConfigFromFiles(caFile, certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that NewReloadableTLSConfig makes a copy so we can re-use the same
|
||||||
|
// base for both client and server with swapped verifiers.
|
||||||
|
tlsCfg.VerifyPeerCertificate = serverVerifyCerts
|
||||||
|
s.serverTLSCfg = NewReloadableTLSConfig(tlsCfg)
|
||||||
|
tlsCfg.VerifyPeerCertificate = clientVerifyCerts
|
||||||
|
s.clientTLSCfg = NewReloadableTLSConfig(tlsCfg)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerTLSConfig returns a *tls.Config that allows any TCP listener to accept
|
||||||
|
// and authorize incoming Connect clients. It will return a single static config
|
||||||
|
// with hooks to dynamically load certificates, and perform Connect
|
||||||
|
// authorization during verification. Service implementations do not need to
|
||||||
|
// reload this to get new certificates.
|
||||||
|
//
|
||||||
|
// At any time it may be possible that the Service instance does not have access
|
||||||
|
// to usable certificates due to not being initially setup yet or a prolonged
|
||||||
|
// error during renewal. The listener will be able to accept connections again
|
||||||
|
// once connectivity is restored provided the client's Token is valid.
|
||||||
|
func (s *Service) ServerTLSConfig() *tls.Config {
|
||||||
|
return s.serverTLSCfg.TLSConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to a remote Connect-enabled server. The passed Resolver is used
|
||||||
|
// to discover a single candidate instance which will be dialled and have it's
|
||||||
|
// TLS certificate verified against the expected identity. Failures are returned
|
||||||
|
// directly with no retries. Repeated dials may use different instances
|
||||||
|
// depending on the Resolver implementation.
|
||||||
|
//
|
||||||
|
// Timeout can be managed via the Context.
|
||||||
|
func (s *Service) Dial(ctx context.Context, resolver Resolver) (net.Conn, error) {
|
||||||
|
addr, certURI, err := resolver.Resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var dialer net.Dialer
|
||||||
|
tcpConn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(tcpConn, s.clientTLSCfg.TLSConfig())
|
||||||
|
// Set deadline for Handshake to complete.
|
||||||
|
deadline, ok := ctx.Deadline()
|
||||||
|
if ok {
|
||||||
|
tlsConn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
err = tlsConn.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
tlsConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Clear deadline since that was only for connection. Caller can set their own
|
||||||
|
// deadline later as necessary.
|
||||||
|
tlsConn.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
// Verify that the connect server's URI matches certURI
|
||||||
|
err = verifyServerCertMatchesURI(tlsConn.ConnectionState().PeerCertificates,
|
||||||
|
certURI)
|
||||||
|
if err != nil {
|
||||||
|
tlsConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPDialContext is compatible with http.Transport.DialContext. It expects the
|
||||||
|
// addr hostname to be specified using Consul DNS query syntax, e.g.
|
||||||
|
// "web.service.consul". It converts that into the equivalent ConsulResolver and
|
||||||
|
// then call s.Dial with the resolver. This is low level, clients should
|
||||||
|
// typically use HTTPClient directly.
|
||||||
|
func (s *Service) HTTPDialContext(ctx context.Context, network,
|
||||||
|
addr string) (net.Conn, error) {
|
||||||
|
var r ConsulResolver
|
||||||
|
// TODO(banks): parse addr into ConsulResolver
|
||||||
|
return s.Dial(ctx, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPClient returns an *http.Client configured to dial remote Consul Connect
|
||||||
|
// HTTP services. The client will return an error if attempting to make requests
|
||||||
|
// to a non HTTPS hostname. It resolves the domain of the request with the same
|
||||||
|
// syntax as Consul DNS queries although it performs discovery directly via the
|
||||||
|
// API rather than just relying on Consul DNS. Hostnames that are not valid
|
||||||
|
// Consul DNS queries will fail.
|
||||||
|
func (s *Service) HTTPClient() *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: s.HTTPDialContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the service and frees resources.
|
||||||
|
func (s *Service) Close() {
|
||||||
|
// TODO(banks): stop background activity if started
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
package connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_Dial(t *testing.T) {
|
||||||
|
ca := connect.TestCA(t, nil)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
accept bool
|
||||||
|
handshake bool
|
||||||
|
presentService string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "working",
|
||||||
|
accept: true,
|
||||||
|
handshake: true,
|
||||||
|
presentService: "db",
|
||||||
|
wantErr: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp connect fail",
|
||||||
|
accept: false,
|
||||||
|
handshake: false,
|
||||||
|
presentService: "db",
|
||||||
|
wantErr: "connection refused",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "handshake timeout",
|
||||||
|
accept: true,
|
||||||
|
handshake: false,
|
||||||
|
presentService: "db",
|
||||||
|
wantErr: "i/o timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad cert",
|
||||||
|
accept: true,
|
||||||
|
handshake: true,
|
||||||
|
presentService: "web",
|
||||||
|
wantErr: "peer certificate mismatch",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
s, err := NewService("web", nil)
|
||||||
|
require.Nil(err)
|
||||||
|
|
||||||
|
// Force TLSConfig
|
||||||
|
s.clientTLSCfg = NewReloadableTLSConfig(TestTLSConfig(t, "web", ca))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(),
|
||||||
|
100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
testSvc := NewTestService(t, tt.presentService, ca)
|
||||||
|
testSvc.TimeoutHandshake = !tt.handshake
|
||||||
|
|
||||||
|
if tt.accept {
|
||||||
|
go func() {
|
||||||
|
err := testSvc.Serve()
|
||||||
|
require.Nil(err)
|
||||||
|
}()
|
||||||
|
defer testSvc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always expect to be connecting to a "DB"
|
||||||
|
resolver := &StaticResolver{
|
||||||
|
Addr: testSvc.Addr,
|
||||||
|
CertURI: connect.TestSpiffeIDService(t, "db"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// All test runs should complete in under 500ms due to the timeout about.
|
||||||
|
// Don't wait for whole test run to get stuck.
|
||||||
|
testTimeout := 500 * time.Millisecond
|
||||||
|
testTimer := time.AfterFunc(testTimeout, func() {
|
||||||
|
panic(fmt.Sprintf("test timed out after %s", testTimeout))
|
||||||
|
})
|
||||||
|
|
||||||
|
conn, err := s.Dial(ctx, resolver)
|
||||||
|
testTimer.Stop()
|
||||||
|
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
require.Nil(err)
|
||||||
|
} else {
|
||||||
|
require.NotNil(err)
|
||||||
|
require.Contains(err.Error(), tt.wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICIDCCAcagAwIBAgIBATAKBggqhkjOPQQDAjAaMRgwFgYDVQQDEw9Db25zdWwg
|
|
||||||
SW50ZXJuYWwwHhcNMTgwMzIzMjIwNDI1WhcNMjgwMzIwMjIwNDI1WjAaMRgwFgYD
|
|
||||||
VQQDEw9Db25zdWwgSW50ZXJuYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT3
|
|
||||||
IPiDHugKYEVaSpIzBjqU5lQrmirC6N1XHyOAhF2psGGxcxezpf8Vgy5Iv6XbmeHr
|
|
||||||
cttyzUYtUKhrFBhxkPYRo4H8MIH5MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E
|
|
||||||
BTADAQH/MCkGA1UdDgQiBCCrnNQy2IQS73Co9WbrPXtq/YP9SvIBOJ8iYRWTOxjC
|
|
||||||
qTArBgNVHSMEJDAigCCrnNQy2IQS73Co9WbrPXtq/YP9SvIBOJ8iYRWTOxjCqTA/
|
|
||||||
BgNVHREEODA2hjRzcGlmZmU6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1
|
|
||||||
NTU1NTU1NTUuY29uc3VsMD0GA1UdHgEB/wQzMDGgLzAtgisxMTExMTExMS0yMjIy
|
|
||||||
LTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49BAMCA0gAMEUC
|
|
||||||
IQDwWL6ZuszKrZjSJwDzdhRQtj1ppezJrKaDTJx+4F/tyQIgEaQCR935ztIqZzgO
|
|
||||||
Ka6ozcH2Ubd4j4cDC1XswVMW6zs=
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIDUDO3I7WKbLTTWkNKA4unB2RLq/RX+L+XIFssDE/AD7oAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAE9yD4gx7oCmBFWkqSMwY6lOZUK5oqwujdVx8jgIRdqbBhsXMXs6X/
|
|
||||||
FYMuSL+l25nh63Lbcs1GLVCoaxQYcZD2EQ==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,14 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICEDCCAbagAwIBAgIBBTAKBggqhkjOPQQDAjAaMRgwFgYDVQQDEw9Db25zdWwg
|
|
||||||
SW50ZXJuYWwwHhcNMTgwMzIzMjIwNDI1WhcNMjgwMzIwMjIwNDI1WjAQMQ4wDAYD
|
|
||||||
VQQDEwVjYWNoZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOWw8369v4DHJAI6
|
|
||||||
k061hU8rxaQs87mZFQ52JfleJjRoDUuZIPLhZHMFbvbI8pDWi7YdjluNbzNNh6nu
|
|
||||||
fAivylujgfYwgfMwDgYDVR0PAQH/BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
|
||||||
BggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCCHhMqV2/R8meSsXtwh
|
|
||||||
OLC9hP7WQfuvwJ6V6uwKZdEofTArBgNVHSMEJDAigCCrnNQy2IQS73Co9WbrPXtq
|
|
||||||
/YP9SvIBOJ8iYRWTOxjCqTBcBgNVHREEVTBThlFzcGlmZmU6Ly8xMTExMTExMS0y
|
|
||||||
MjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsL25zL2RlZmF1bHQvZGMv
|
|
||||||
ZGMwMS9zdmMvY2FjaGUwCgYIKoZIzj0EAwIDSAAwRQIgPfekKBd/ltpVkdjnB0Hp
|
|
||||||
cV9HPwy12tXp4suR2nspSNkCIQD1Th/hvxuBKkRYy9Bl+jgTbrFdd4fLCWPeFbaM
|
|
||||||
sgLK7g==
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIPTSPV2cWNnO69f+vYyCg5frpoBtK6L+kZVLrGCv3TdnoAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAE5bDzfr2/gMckAjqTTrWFTyvFpCzzuZkVDnYl+V4mNGgNS5kg8uFk
|
|
||||||
cwVu9sjykNaLth2OW41vM02Hqe58CK/KWw==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,13 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICCjCCAbCgAwIBAgIBBDAKBggqhkjOPQQDAjAaMRgwFgYDVQQDEw9Db25zdWwg
|
|
||||||
SW50ZXJuYWwwHhcNMTgwMzIzMjIwNDI1WhcNMjgwMzIwMjIwNDI1WjANMQswCQYD
|
|
||||||
VQQDEwJkYjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEcTyr2l7yYWZuh++02M
|
|
||||||
usR20QrZtHdd7goKmYrIpQ3ekmHuLLgJWgTTaIhCj8fzbryep+s8oM7EiPhRQ14l
|
|
||||||
uSujgfMwgfAwDgYDVR0PAQH/BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr
|
|
||||||
BgEFBQcDATAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCAy6jHCBBT2bii+aMJCDJ33
|
|
||||||
bFJtR72bxDBUi5b+YWyWwDArBgNVHSMEJDAigCCrnNQy2IQS73Co9WbrPXtq/YP9
|
|
||||||
SvIBOJ8iYRWTOxjCqTBZBgNVHREEUjBQhk5zcGlmZmU6Ly8xMTExMTExMS0yMjIy
|
|
||||||
LTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsL25zL2RlZmF1bHQvZGMvZGMw
|
|
||||||
MS9zdmMvZGIwCgYIKoZIzj0EAwIDSAAwRQIhALCW4cOEpuYfLJ0NGwEmYG5Fko0N
|
|
||||||
WMccL0gEQzKUbIWrAiAIw8wkTSf1O8vTHeKdR1fCmdVoDRFRKB643PaofUzFxA==
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIMHv1pjt75IjKXzl8l4rBtEFS1pEuOM4WNgeHg5Qn1RroAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAERxPKvaXvJhZm6H77TYy6xHbRCtm0d13uCgqZisilDd6SYe4suAla
|
|
||||||
BNNoiEKPx/NuvJ6n6zygzsSI+FFDXiW5Kw==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,13 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICDDCCAbKgAwIBAgIBAzAKBggqhkjOPQQDAjAaMRgwFgYDVQQDEw9Db25zdWwg
|
|
||||||
SW50ZXJuYWwwHhcNMTgwMzIzMjIwNDI1WhcNMjgwMzIwMjIwNDI1WjAOMQwwCgYD
|
|
||||||
VQQDEwN3ZWIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARF47lERGXziNBC74Kh
|
|
||||||
U3W29/M7JO9LIUaJgK0LJbhgf0MuPxf7gX+PnxH5ImI5yfXRv0SSxeCq7377IkXP
|
|
||||||
XS6Fo4H0MIHxMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAgYI
|
|
||||||
KwYBBQUHAwEwDAYDVR0TAQH/BAIwADApBgNVHQ4EIgQg26hfNYiVwYRm7CQJvdOd
|
|
||||||
NIOmG3t8vNwXCtktC782cf8wKwYDVR0jBCQwIoAgq5zUMtiEEu9wqPVm6z17av2D
|
|
||||||
/UryATifImEVkzsYwqkwWgYDVR0RBFMwUYZPc3BpZmZlOi8vMTExMTExMTEtMjIy
|
|
||||||
Mi0zMzMzLTQ0NDQtNTU1NTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2Rj
|
|
||||||
MDEvc3ZjL3dlYjAKBggqhkjOPQQDAgNIADBFAiAzi8uBs+ApPfAZZm5eO/hhVZiv
|
|
||||||
E8p84VKCqPeF3tFfoAIhANVkdSnp2AKU5T7SlJHmieu3DFNWCVpajlHJvf286J94
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIPOIj4BFS0fknG+uAVKZIWRpnzp7O3OKpBDgEmuml7lcoAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEReO5RERl84jQQu+CoVN1tvfzOyTvSyFGiYCtCyW4YH9DLj8X+4F/
|
|
||||||
j58R+SJiOcn10b9EksXgqu9++yJFz10uhQ==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,14 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICDDCCAbKgAwIBAgIBAjAKBggqhkjOPQQDAjAQMQ4wDAYDVQQDEwVWYXVsdDAe
|
|
||||||
Fw0xODAzMjMyMjA0MjVaFw0yODAzMjAyMjA0MjVaMBAxDjAMBgNVBAMTBVZhdWx0
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAjGVnRy/7Q2SU4ePbKbsurRAHKYA
|
|
||||||
CuA3r9QrowgZOr7yptF54shiobMIORpfKYkoYkhzL1lhWKI06BUJ4xuPd6OB/DCB
|
|
||||||
+TAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zApBgNVHQ4EIgQgqEc5
|
|
||||||
ZrELD5ySxapbU+eRb+aEv1MEoCvjC0mCA1uJecMwKwYDVR0jBCQwIoAgqEc5ZrEL
|
|
||||||
D5ySxapbU+eRb+aEv1MEoCvjC0mCA1uJecMwPwYDVR0RBDgwNoY0c3BpZmZlOi8v
|
|
||||||
MTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1NTU1LmNvbnN1bDA9BgNV
|
|
||||||
HR4BAf8EMzAxoC8wLYIrMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1
|
|
||||||
NTU1LmNvbnN1bDAKBggqhkjOPQQDAgNIADBFAiEA6pBdeglhq//A7sYnYk85XL+3
|
|
||||||
4IDrXrGN3KjC9qo3J9ICIDS9pEoTPWAWDfn1ccPafKVBrJm6KrmljcvymQ2QUDIZ
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIKnuCctuvtyzf+M6B8jGqejG4T5o7NMRYjO2M3dZITCboAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEAjGVnRy/7Q2SU4ePbKbsurRAHKYACuA3r9QrowgZOr7yptF54shi
|
|
||||||
obMIORpfKYkoYkhzL1lhWKI06BUJ4xuPdw==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,13 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICBzCCAaygAwIBAgIBCDAKBggqhkjOPQQDAjAQMQ4wDAYDVQQDEwVWYXVsdDAe
|
|
||||||
Fw0xODAzMjMyMjA0MjVaFw0yODAzMjAyMjA0MjVaMBAxDjAMBgNVBAMTBWNhY2hl
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyB6D+Eqi/71EhUrBWlcZOV2vjS9Y
|
|
||||||
xnUQ3jfH+QUZur7WOuGLnO7eArbAHcDbqKGyDWxlkZH04sGYOXaEW7UUd6OB9jCB
|
|
||||||
8zAOBgNVHQ8BAf8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
|
|
||||||
MAwGA1UdEwEB/wQCMAAwKQYDVR0OBCIEIGapiHFxlbYbNKFlwdPMpKhIypvNZXo8
|
|
||||||
k/OZLki/vurQMCsGA1UdIwQkMCKAIKhHOWaxCw+cksWqW1PnkW/mhL9TBKAr4wtJ
|
|
||||||
ggNbiXnDMFwGA1UdEQRVMFOGUXNwaWZmZTovLzExMTExMTExLTIyMjItMzMzMy00
|
|
||||||
NDQ0LTU1NTU1NTU1NTU1NS5jb25zdWwvbnMvZGVmYXVsdC9kYy9kYzAxL3N2Yy9j
|
|
||||||
YWNoZTAKBggqhkjOPQQDAgNJADBGAiEA/vRLXbkigS6l89MxFk0RFE7Zo4vorv7s
|
|
||||||
E1juCOsVJBICIQDXlpmYH9fPon6DYMyOxQttNjkuWbJgnPv7rPg+CllRyA==
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIEbQOv4odF2Tu8ZnJTJuytvOd2HOF9HxgGw5ei1pkP4moAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEyB6D+Eqi/71EhUrBWlcZOV2vjS9YxnUQ3jfH+QUZur7WOuGLnO7e
|
|
||||||
ArbAHcDbqKGyDWxlkZH04sGYOXaEW7UUdw==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,13 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICADCCAaagAwIBAgIBBzAKBggqhkjOPQQDAjAQMQ4wDAYDVQQDEwVWYXVsdDAe
|
|
||||||
Fw0xODAzMjMyMjA0MjVaFw0yODAzMjAyMjA0MjVaMA0xCzAJBgNVBAMTAmRiMFkw
|
|
||||||
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFeB4DynO6IeKOE4zFLlBVFv+4HeWRvK3
|
|
||||||
6cQ9L6v5uhLfdcYyqhT/QLbQ4R8ks1vUTTiq0XJsAGdkvkt71fiEl6OB8zCB8DAO
|
|
||||||
BgNVHQ8BAf8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwG
|
|
||||||
A1UdEwEB/wQCMAAwKQYDVR0OBCIEIKjVz8n91cej8q6WpDNd0hwSMAE2ddY056PH
|
|
||||||
hMfaBM6GMCsGA1UdIwQkMCKAIKhHOWaxCw+cksWqW1PnkW/mhL9TBKAr4wtJggNb
|
|
||||||
iXnDMFkGA1UdEQRSMFCGTnNwaWZmZTovLzExMTExMTExLTIyMjItMzMzMy00NDQ0
|
|
||||||
LTU1NTU1NTU1NTU1NS5jb25zdWwvbnMvZGVmYXVsdC9kYy9kYzAxL3N2Yy9kYjAK
|
|
||||||
BggqhkjOPQQDAgNIADBFAiAdYkokbeZr7W32NhjcNoTMNwpz9CqJpK6Yzu4N6EJc
|
|
||||||
pAIhALHpRM57zdiMouDOlhGPX5XKzbSl2AnBjFvbPqgFV09Z
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIHnzia+DNTFB7uYQEuWvLR2czGCuDfOTt1FfcBo1uBJioAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEFeB4DynO6IeKOE4zFLlBVFv+4HeWRvK36cQ9L6v5uhLfdcYyqhT/
|
|
||||||
QLbQ4R8ks1vUTTiq0XJsAGdkvkt71fiElw==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,13 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICAjCCAaigAwIBAgIBBjAKBggqhkjOPQQDAjAQMQ4wDAYDVQQDEwVWYXVsdDAe
|
|
||||||
Fw0xODAzMjMyMjA0MjVaFw0yODAzMjAyMjA0MjVaMA4xDDAKBgNVBAMTA3dlYjBZ
|
|
||||||
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABM9XzxWFCa80uQDfJEGboUC15Yr+FwDp
|
|
||||||
OemThalQxFpkL7gQSIgpzgGULIx+jCiu+clJ0QhbWT2dnS8vFUKq35qjgfQwgfEw
|
|
||||||
DgYDVR0PAQH/BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAM
|
|
||||||
BgNVHRMBAf8EAjAAMCkGA1UdDgQiBCCN+TKHPCOr48hxRCx4rqbWQg5QHkCSNzjZ
|
|
||||||
qi1JGs13njArBgNVHSMEJDAigCCoRzlmsQsPnJLFqltT55Fv5oS/UwSgK+MLSYID
|
|
||||||
W4l5wzBaBgNVHREEUzBRhk9zcGlmZmU6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0
|
|
||||||
NC01NTU1NTU1NTU1NTUuY29uc3VsL25zL2RlZmF1bHQvZGMvZGMwMS9zdmMvd2Vi
|
|
||||||
MAoGCCqGSM49BAMCA0gAMEUCIBd6gpL6E8rms5BU+cJeeyv0Rjc18edn2g3q2wLN
|
|
||||||
r1zAAiEAv16whKwR0DeKkldGLDQIu9nCNvfDZrEWgywIBYbzLxY=
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIOCMjjRexX3qHjixpRwLxggJd9yuskqUoPy8/MepafP+oAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEz1fPFYUJrzS5AN8kQZuhQLXliv4XAOk56ZOFqVDEWmQvuBBIiCnO
|
|
||||||
AZQsjH6MKK75yUnRCFtZPZ2dLy8VQqrfmg==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -1,14 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICFjCCAbygAwIBAgIBAjAKBggqhkjOPQQDAjAaMRgwFgYDVQQDEw9Db25zdWwg
|
|
||||||
SW50ZXJuYWwwHhcNMTgwMzIzMjIwNDI1WhcNMjgwMzIwMjIwNDI1WjAQMQ4wDAYD
|
|
||||||
VQQDEwVWYXVsdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAIxlZ0cv+0NklOH
|
|
||||||
j2ym7Lq0QBymAArgN6/UK6MIGTq+8qbReeLIYqGzCDkaXymJKGJIcy9ZYViiNOgV
|
|
||||||
CeMbj3ejgfwwgfkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wKQYD
|
|
||||||
VR0OBCIEIKhHOWaxCw+cksWqW1PnkW/mhL9TBKAr4wtJggNbiXnDMCsGA1UdIwQk
|
|
||||||
MCKAIKuc1DLYhBLvcKj1Zus9e2r9g/1K8gE4nyJhFZM7GMKpMD8GA1UdEQQ4MDaG
|
|
||||||
NHNwaWZmZTovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTU1NTU1NTU1NTU1NS5j
|
|
||||||
b25zdWwwPQYDVR0eAQH/BDMwMaAvMC2CKzExMTExMTExLTIyMjItMzMzMy00NDQ0
|
|
||||||
LTU1NTU1NTU1NTU1NS5jb25zdWwwCgYIKoZIzj0EAwIDSAAwRQIgWWWj8/6SaY2y
|
|
||||||
wzOtIphwZLewCuLMG6KG8uY4S7UsosgCIQDhCbT/LUKq/A21khQncBmM79ng9Gbx
|
|
||||||
/4Zw8zbVmnZJKg==
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,243 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math/big"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// You can verify a given leaf with a given root using:
|
|
||||||
//
|
|
||||||
// $ openssl verify -verbose -CAfile ca2-ca-vault.cert.pem ca1-svc-db.cert.pem
|
|
||||||
//
|
|
||||||
// Note that to verify via the cross-signed intermediate, openssl requires it to
|
|
||||||
// be bundled with the _root_ CA bundle and will ignore the cert if it's passed
|
|
||||||
// with the subject. You can do that with:
|
|
||||||
//
|
|
||||||
// $ openssl verify -verbose -CAfile \
|
|
||||||
// <(cat ca1-ca-consul-internal.cert.pem ca2-xc-by-ca1.cert.pem) \
|
|
||||||
// ca2-svc-db.cert.pem
|
|
||||||
// ca2-svc-db.cert.pem: OK
|
|
||||||
//
|
|
||||||
// Note that the same leaf and root without the intermediate should fail:
|
|
||||||
//
|
|
||||||
// $ openssl verify -verbose -CAfile ca1-ca-consul-internal.cert.pem ca2-svc-db.cert.pem
|
|
||||||
// ca2-svc-db.cert.pem: CN = db
|
|
||||||
// error 20 at 0 depth lookup:unable to get local issuer certificate
|
|
||||||
//
|
|
||||||
// NOTE: THIS IS A QUIRK OF OPENSSL; in Connect we will distribute the roots
|
|
||||||
// alone and stable intermediates like the XC cert to the _leaf_.
|
|
||||||
|
|
||||||
var clusterID = "11111111-2222-3333-4444-555555555555"
|
|
||||||
var cAs = []string{"Consul Internal", "Vault"}
|
|
||||||
var services = []string{"web", "db", "cache"}
|
|
||||||
var slugRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
|
||||||
var serial int64
|
|
||||||
|
|
||||||
type caInfo struct {
|
|
||||||
id int
|
|
||||||
name string
|
|
||||||
slug string
|
|
||||||
uri *url.URL
|
|
||||||
pk *ecdsa.PrivateKey
|
|
||||||
cert *x509.Certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Make CA certs
|
|
||||||
caInfos := make(map[string]caInfo)
|
|
||||||
var previousCA *caInfo
|
|
||||||
for idx, name := range cAs {
|
|
||||||
ca := caInfo{
|
|
||||||
id: idx + 1,
|
|
||||||
name: name,
|
|
||||||
slug: strings.ToLower(slugRe.ReplaceAllString(name, "-")),
|
|
||||||
}
|
|
||||||
pk, err := makePK(fmt.Sprintf("ca%d-ca-%s.key.pem", ca.id, ca.slug))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
ca.pk = pk
|
|
||||||
caURI, err := url.Parse(fmt.Sprintf("spiffe://%s.consul", clusterID))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
ca.uri = caURI
|
|
||||||
cert, err := makeCACert(ca, previousCA)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
ca.cert = cert
|
|
||||||
caInfos[name] = ca
|
|
||||||
previousCA = &ca
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each CA, make a leaf cert for each service
|
|
||||||
for _, ca := range caInfos {
|
|
||||||
for _, svc := range services {
|
|
||||||
_, err := makeLeafCert(ca, svc)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePK(path string) (*ecdsa.PrivateKey, error) {
|
|
||||||
log.Printf("Writing PK file: %s", path)
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, err := x509.MarshalECPrivateKey(priv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writePEM(path, "EC PRIVATE KEY", bs)
|
|
||||||
return priv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCACert(ca caInfo, previousCA *caInfo) (*x509.Certificate, error) {
|
|
||||||
path := fmt.Sprintf("ca%d-ca-%s.cert.pem", ca.id, ca.slug)
|
|
||||||
log.Printf("Writing CA cert file: %s", path)
|
|
||||||
serial++
|
|
||||||
subj := pkix.Name{
|
|
||||||
CommonName: ca.name,
|
|
||||||
}
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(serial),
|
|
||||||
Subject: subj,
|
|
||||||
// New in go 1.10
|
|
||||||
URIs: []*url.URL{ca.uri},
|
|
||||||
// Add DNS name constraint
|
|
||||||
PermittedDNSDomainsCritical: true,
|
|
||||||
PermittedDNSDomains: []string{ca.uri.Hostname()},
|
|
||||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
|
|
||||||
IsCA: true,
|
|
||||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
AuthorityKeyId: keyID(&ca.pk.PublicKey),
|
|
||||||
SubjectKeyId: keyID(&ca.pk.PublicKey),
|
|
||||||
}
|
|
||||||
bs, err := x509.CreateCertificate(rand.Reader, &template, &template,
|
|
||||||
&ca.pk.PublicKey, ca.pk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writePEM(path, "CERTIFICATE", bs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(bs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if previousCA != nil {
|
|
||||||
// Also create cross-signed cert as we would use during rotation between
|
|
||||||
// previous CA and this one.
|
|
||||||
template.AuthorityKeyId = keyID(&previousCA.pk.PublicKey)
|
|
||||||
bs, err := x509.CreateCertificate(rand.Reader, &template,
|
|
||||||
previousCA.cert, &ca.pk.PublicKey, previousCA.pk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := fmt.Sprintf("ca%d-xc-by-ca%d.cert.pem", ca.id, previousCA.id)
|
|
||||||
err = writePEM(path, "CERTIFICATE", bs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cert, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyID(pub *ecdsa.PublicKey) []byte {
|
|
||||||
// This is not standard; RFC allows any unique identifier as long as they
|
|
||||||
// match in subject/authority chains but suggests specific hashing of DER
|
|
||||||
// bytes of public key including DER tags. I can't be bothered to do esp.
|
|
||||||
// since ECDSA keys don't have a handy way to marshal the publick key alone.
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write(pub.X.Bytes())
|
|
||||||
h.Write(pub.Y.Bytes())
|
|
||||||
return h.Sum([]byte{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeLeafCert(ca caInfo, svc string) (*x509.Certificate, error) {
|
|
||||||
svcURI := ca.uri
|
|
||||||
svcURI.Path = "/ns/default/dc/dc01/svc/" + svc
|
|
||||||
|
|
||||||
keyPath := fmt.Sprintf("ca%d-svc-%s.key.pem", ca.id, svc)
|
|
||||||
cPath := fmt.Sprintf("ca%d-svc-%s.cert.pem", ca.id, svc)
|
|
||||||
|
|
||||||
pk, err := makePK(keyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Writing Service Cert: %s", cPath)
|
|
||||||
|
|
||||||
serial++
|
|
||||||
subj := pkix.Name{
|
|
||||||
CommonName: svc,
|
|
||||||
}
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(serial),
|
|
||||||
Subject: subj,
|
|
||||||
// New in go 1.10
|
|
||||||
URIs: []*url.URL{svcURI},
|
|
||||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
KeyUsage: x509.KeyUsageDataEncipherment |
|
|
||||||
x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature |
|
|
||||||
x509.KeyUsageKeyEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
||||||
x509.ExtKeyUsageClientAuth,
|
|
||||||
x509.ExtKeyUsageServerAuth,
|
|
||||||
},
|
|
||||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
AuthorityKeyId: keyID(&ca.pk.PublicKey),
|
|
||||||
SubjectKeyId: keyID(&pk.PublicKey),
|
|
||||||
}
|
|
||||||
bs, err := x509.CreateCertificate(rand.Reader, &template, ca.cert,
|
|
||||||
&pk.PublicKey, ca.pk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writePEM(cPath, "CERTIFICATE", bs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.ParseCertificate(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePEM(name, typ string, bs []byte) error {
|
|
||||||
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return pem.Encode(f, &pem.Block{Type: typ, Bytes: bs})
|
|
||||||
}
|
|
|
@ -4,30 +4,155 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"path"
|
"net"
|
||||||
"path/filepath"
|
"sync/atomic"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-testing-interface"
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/lib/freeport"
|
||||||
|
testing "github.com/mitchellh/go-testing-interface"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testDataDir is a janky temporary hack to allow use of these methods from
|
// testVerifier creates a helper verifyFunc that can be set in a tls.Config and
|
||||||
// proxy package. We need to revisit where all this lives since it logically
|
// records calls made, passing back the certificates presented via the returned
|
||||||
// overlaps with consul/agent in Mitchell's PR and that one generates certs on
|
// channel. The channel is buffered so up to 128 verification calls can be made
|
||||||
// the fly which will make this unecessary but I want to get things working for
|
// without reading the chan before verification blocks.
|
||||||
// now with what I've got :). This wonderful heap kinda-sorta gets the path
|
func testVerifier(t testing.T, returnErr error) (verifyFunc, chan [][]byte) {
|
||||||
// relative to _this_ file so it works even if the Test* method is being called
|
ch := make(chan [][]byte, 128)
|
||||||
// from a test binary in another package dir.
|
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||||
func testDataDir() string {
|
ch <- rawCerts
|
||||||
_, filename, _, ok := runtime.Caller(0)
|
return returnErr
|
||||||
if !ok {
|
}, ch
|
||||||
panic("no caller information")
|
|
||||||
}
|
|
||||||
return path.Dir(filename) + "/testdata"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestTLSConfig returns a *tls.Config suitable for use during tests.
|
||||||
|
func TestTLSConfig(t testing.T, service string, ca *structs.CARoot) *tls.Config {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Insecure default (nil verifier)
|
||||||
|
cfg := defaultTLSConfig(nil)
|
||||||
|
cfg.Certificates = []tls.Certificate{TestSvcKeyPair(t, service, ca)}
|
||||||
|
cfg.RootCAs = TestCAPool(t, ca)
|
||||||
|
cfg.ClientCAs = TestCAPool(t, ca)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCAPool returns an *x509.CertPool containing the passed CA's root(s)
|
||||||
|
func TestCAPool(t testing.T, cas ...*structs.CARoot) *x509.CertPool {
|
||||||
|
t.Helper()
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
for _, ca := range cas {
|
||||||
|
pool.AppendCertsFromPEM([]byte(ca.RootCert))
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSvcKeyPair returns an tls.Certificate containing both cert and private
|
||||||
|
// key for a given service under a given CA from the testdata dir.
|
||||||
|
func TestSvcKeyPair(t testing.T, service string, ca *structs.CARoot) tls.Certificate {
|
||||||
|
t.Helper()
|
||||||
|
certPEM, keyPEM := connect.TestLeaf(t, service, ca)
|
||||||
|
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
||||||
|
require.Nil(t, err)
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPeerCertificates returns a []*x509.Certificate as you'd get from
|
||||||
|
// tls.Conn.ConnectionState().PeerCertificates including the named certificate.
|
||||||
|
func TestPeerCertificates(t testing.T, service string, ca *structs.CARoot) []*x509.Certificate {
|
||||||
|
t.Helper()
|
||||||
|
certPEM, _ := connect.TestLeaf(t, service, ca)
|
||||||
|
cert, err := connect.ParseCert(certPEM)
|
||||||
|
require.Nil(t, err)
|
||||||
|
return []*x509.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestService runs a service listener that can be used to test clients. It's
|
||||||
|
// behaviour can be controlled by the struct members.
|
||||||
|
type TestService struct {
|
||||||
|
// The service name to serve.
|
||||||
|
Service string
|
||||||
|
// The (test) CA to use for generating certs.
|
||||||
|
CA *structs.CARoot
|
||||||
|
// TimeoutHandshake controls whether the listening server will complete a TLS
|
||||||
|
// handshake quickly enough.
|
||||||
|
TimeoutHandshake bool
|
||||||
|
// TLSCfg is the tls.Config that will be used. By default it's set up from the
|
||||||
|
// service and ca set.
|
||||||
|
TLSCfg *tls.Config
|
||||||
|
// Addr is the listen address. It is set to a random free port on `localhost`
|
||||||
|
// by default.
|
||||||
|
Addr string
|
||||||
|
|
||||||
|
l net.Listener
|
||||||
|
stopFlag int32
|
||||||
|
stopChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestService returns a TestService. It should be closed when test is
|
||||||
|
// complete.
|
||||||
|
func NewTestService(t testing.T, service string, ca *structs.CARoot) *TestService {
|
||||||
|
ports := freeport.GetT(t, 1)
|
||||||
|
return &TestService{
|
||||||
|
Service: service,
|
||||||
|
CA: ca,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
TLSCfg: TestTLSConfig(t, service, ca),
|
||||||
|
Addr: fmt.Sprintf("localhost:%d", ports[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve runs a TestService and blocks until it is closed or errors.
|
||||||
|
func (s *TestService) Serve() error {
|
||||||
|
// Just accept TCP conn but so we can control timing of accept/handshake
|
||||||
|
l, err := net.Listen("tcp", s.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.l = l
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := s.l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if atomic.LoadInt32(&s.stopFlag) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore the conn if we are not actively ha
|
||||||
|
if !s.TimeoutHandshake {
|
||||||
|
// Upgrade conn to TLS
|
||||||
|
conn = tls.Server(conn, s.TLSCfg)
|
||||||
|
|
||||||
|
// Run an echo service
|
||||||
|
go io.Copy(conn, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close this conn when we stop
|
||||||
|
go func(c net.Conn) {
|
||||||
|
<-s.stopChan
|
||||||
|
c.Close()
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops a TestService
|
||||||
|
func (s *TestService) Close() {
|
||||||
|
old := atomic.SwapInt32(&s.stopFlag, 1)
|
||||||
|
if old == 0 {
|
||||||
|
if s.l != nil {
|
||||||
|
s.l.Close()
|
||||||
|
}
|
||||||
|
close(s.stopChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// TestCAPool returns an *x509.CertPool containing the named CA certs from the
|
// TestCAPool returns an *x509.CertPool containing the named CA certs from the
|
||||||
// testdata dir.
|
// testdata dir.
|
||||||
func TestCAPool(t testing.T, caNames ...string) *x509.CertPool {
|
func TestCAPool(t testing.T, caNames ...string) *x509.CertPool {
|
||||||
|
@ -86,3 +211,4 @@ func (a *TestAuther) Auth(rawCerts [][]byte,
|
||||||
verifiedChains [][]*x509.Certificate) error {
|
verifiedChains [][]*x509.Certificate) error {
|
||||||
return a.Return
|
return a.Return
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
120
connect/tls.go
120
connect/tls.go
|
@ -3,13 +3,18 @@ package connect
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultTLSConfig returns the standard config for connect clients and servers.
|
// verifyFunc is the type of tls.Config.VerifyPeerCertificate for convenience.
|
||||||
func defaultTLSConfig() *tls.Config {
|
type verifyFunc func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
|
||||||
serverAuther := &ServerAuther{}
|
|
||||||
|
// defaultTLSConfig returns the standard config.
|
||||||
|
func defaultTLSConfig(verify verifyFunc) *tls.Config {
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
@ -29,16 +34,18 @@ func defaultTLSConfig() *tls.Config {
|
||||||
// We have to set this since otherwise Go will attempt to verify DNS names
|
// We have to set this since otherwise Go will attempt to verify DNS names
|
||||||
// match DNS SAN/CN which we don't want. We hook up VerifyPeerCertificate to
|
// match DNS SAN/CN which we don't want. We hook up VerifyPeerCertificate to
|
||||||
// do our own path validation as well as Connect AuthZ.
|
// do our own path validation as well as Connect AuthZ.
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
// By default auth as if we are a server. Clients need to override this with
|
VerifyPeerCertificate: verify,
|
||||||
// an Auther that is performs correct validation of the server identity they
|
// Include h2 to allow connect http servers to automatically support http2.
|
||||||
// intended to connect to.
|
// See: https://github.com/golang/go/blob/917c33fe8672116b04848cf11545296789cafd3b/src/net/http/server.go#L2724-L2731
|
||||||
VerifyPeerCertificate: serverAuther.Auth,
|
NextProtos: []string{"h2"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadableTLSConfig exposes a tls.Config that can have it's certificates
|
// ReloadableTLSConfig exposes a tls.Config that can have it's certificates
|
||||||
// reloaded. This works by
|
// reloaded. On a server, this uses GetConfigForClient to pass the current
|
||||||
|
// tls.Config or client certificate for each acceptted connection. On a client,
|
||||||
|
// this uses GetClientCertificate to provide the current client certificate.
|
||||||
type ReloadableTLSConfig struct {
|
type ReloadableTLSConfig struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
|
@ -46,52 +53,40 @@ type ReloadableTLSConfig struct {
|
||||||
cfg *tls.Config
|
cfg *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReloadableTLSConfig returns a reloadable config currently set to base. The
|
// NewReloadableTLSConfig returns a reloadable config currently set to base.
|
||||||
// Auther used to verify certificates for incoming connections on a Server will
|
|
||||||
// just be copied from the VerifyPeerCertificate passed. Clients will need to
|
|
||||||
// pass a specific Auther instance when they call TLSConfig that is configured
|
|
||||||
// to perform the necessary validation of the server's identity.
|
|
||||||
func NewReloadableTLSConfig(base *tls.Config) *ReloadableTLSConfig {
|
func NewReloadableTLSConfig(base *tls.Config) *ReloadableTLSConfig {
|
||||||
return &ReloadableTLSConfig{cfg: base}
|
c := &ReloadableTLSConfig{}
|
||||||
|
c.SetTLSConfig(base)
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerTLSConfig returns a *tls.Config that will dynamically load certs for
|
// TLSConfig returns a *tls.Config that will dynamically load certs. It's
|
||||||
// each inbound connection via the GetConfigForClient callback.
|
// suitable for use in either a client or server.
|
||||||
func (c *ReloadableTLSConfig) ServerTLSConfig() *tls.Config {
|
func (c *ReloadableTLSConfig) TLSConfig() *tls.Config {
|
||||||
// Setup the basic one with current params even though we will be using
|
|
||||||
// different config for each new conn.
|
|
||||||
c.mu.Lock()
|
|
||||||
base := c.cfg
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// Dynamically fetch the current config for each new inbound connection
|
|
||||||
base.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
|
||||||
return c.TLSConfig(nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSConfig returns the current value for the config. It is safe to call from
|
|
||||||
// any goroutine. The passed Auther is inserted into the config's
|
|
||||||
// VerifyPeerCertificate. Passing a nil Auther will leave the default one in the
|
|
||||||
// base config
|
|
||||||
func (c *ReloadableTLSConfig) TLSConfig(auther Auther) *tls.Config {
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
cfgCopy := c.cfg
|
cfgCopy := c.cfg
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
if auther != nil {
|
|
||||||
cfgCopy.VerifyPeerCertificate = auther.Auth
|
|
||||||
}
|
|
||||||
return cfgCopy
|
return cfgCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTLSConfig sets the config used for future connections. It is safe to call
|
// SetTLSConfig sets the config used for future connections. It is safe to call
|
||||||
// from any goroutine.
|
// from any goroutine.
|
||||||
func (c *ReloadableTLSConfig) SetTLSConfig(cfg *tls.Config) error {
|
func (c *ReloadableTLSConfig) SetTLSConfig(cfg *tls.Config) error {
|
||||||
|
copy := cfg.Clone()
|
||||||
|
copy.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||||
|
current := c.TLSConfig()
|
||||||
|
if len(current.Certificates) < 1 {
|
||||||
|
return nil, errors.New("tls: no certificates configured")
|
||||||
|
}
|
||||||
|
return ¤t.Certificates[0], nil
|
||||||
|
}
|
||||||
|
copy.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
return c.TLSConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.cfg = cfg
|
c.cfg = copy
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +109,8 @@ func devTLSConfigFromFiles(caFile, certFile,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := defaultTLSConfig()
|
// Insecure no verification
|
||||||
|
cfg := defaultTLSConfig(nil)
|
||||||
|
|
||||||
cfg.Certificates = []tls.Certificate{cert}
|
cfg.Certificates = []tls.Certificate{cert}
|
||||||
cfg.RootCAs = roots
|
cfg.RootCAs = roots
|
||||||
|
@ -122,3 +118,43 @@ func devTLSConfigFromFiles(caFile, certFile,
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verifyServerCertMatchesURI is used on tls connections dialled to a connect
|
||||||
|
// server to ensure that the certificate it presented has the correct identity.
|
||||||
|
func verifyServerCertMatchesURI(certs []*x509.Certificate,
|
||||||
|
expected connect.CertURI) error {
|
||||||
|
expectedStr := expected.URI().String()
|
||||||
|
|
||||||
|
if len(certs) < 1 {
|
||||||
|
return errors.New("peer certificate mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only check the first cert assuming this is the only leaf. It's not clear if
|
||||||
|
// services might ever legitimately present multiple leaf certificates or if
|
||||||
|
// the slice is just to allow presenting the whole chain of intermediates.
|
||||||
|
cert := certs[0]
|
||||||
|
|
||||||
|
// Our certs will only ever have a single URI for now so only check that
|
||||||
|
if len(cert.URIs) < 1 {
|
||||||
|
return errors.New("peer certificate mismatch")
|
||||||
|
}
|
||||||
|
// We may want to do better than string matching later in some special
|
||||||
|
// cases and/or encapsulate the "match" logic inside the CertURI
|
||||||
|
// implementation but for now this is all we need.
|
||||||
|
if cert.URIs[0].String() == expectedStr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("peer certificate mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverVerifyCerts is the verifyFunc for use on Connect servers.
|
||||||
|
func serverVerifyCerts(rawCerts [][]byte, chains [][]*x509.Certificate) error {
|
||||||
|
// TODO(banks): implement me
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientVerifyCerts is the verifyFunc for use on Connect clients.
|
||||||
|
func clientVerifyCerts(rawCerts [][]byte, chains [][]*x509.Certificate) error {
|
||||||
|
// TODO(banks): implement me
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,45 +1,103 @@
|
||||||
package connect
|
package connect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/x509"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReloadableTLSConfig(t *testing.T) {
|
func TestReloadableTLSConfig(t *testing.T) {
|
||||||
base := TestTLSConfig(t, "ca1", "web")
|
require := require.New(t)
|
||||||
|
verify, _ := testVerifier(t, nil)
|
||||||
|
base := defaultTLSConfig(verify)
|
||||||
|
|
||||||
c := NewReloadableTLSConfig(base)
|
c := NewReloadableTLSConfig(base)
|
||||||
|
|
||||||
a := &TestAuther{
|
// The dynamic config should be the one we loaded (with some different hooks)
|
||||||
Return: nil,
|
got := c.TLSConfig()
|
||||||
}
|
expect := *base
|
||||||
|
// Equal and even cmp.Diff fail on tls.Config due to unexported fields in
|
||||||
|
// each. Compare a few things to prove it's returning the bits we
|
||||||
|
// specifically set.
|
||||||
|
require.Equal(expect.Certificates, got.Certificates)
|
||||||
|
require.Equal(expect.RootCAs, got.RootCAs)
|
||||||
|
require.Equal(expect.ClientCAs, got.ClientCAs)
|
||||||
|
require.Equal(expect.InsecureSkipVerify, got.InsecureSkipVerify)
|
||||||
|
require.Equal(expect.MinVersion, got.MinVersion)
|
||||||
|
require.Equal(expect.CipherSuites, got.CipherSuites)
|
||||||
|
require.NotNil(got.GetClientCertificate)
|
||||||
|
require.NotNil(got.GetConfigForClient)
|
||||||
|
require.Contains(got.NextProtos, "h2")
|
||||||
|
|
||||||
// The dynamic config should be the one we loaded, but with the passed auther
|
ca := connect.TestCA(t, nil)
|
||||||
expect := base
|
|
||||||
expect.VerifyPeerCertificate = a.Auth
|
|
||||||
require.Equal(t, base, c.TLSConfig(a))
|
|
||||||
|
|
||||||
// The server config should return same too for new connections
|
// Now change the config as if we just loaded certs from Consul
|
||||||
serverCfg := c.ServerTLSConfig()
|
new := TestTLSConfig(t, "web", ca)
|
||||||
require.NotNil(t, serverCfg.GetConfigForClient)
|
err := c.SetTLSConfig(new)
|
||||||
got, err := serverCfg.GetConfigForClient(&tls.ClientHelloInfo{})
|
require.Nil(err)
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, base, got)
|
|
||||||
|
|
||||||
// Now change the config as if we just rotated to a new CA
|
// Change the passed config to ensure SetTLSConfig made a copy otherwise this
|
||||||
new := TestTLSConfig(t, "ca2", "web")
|
// is racey.
|
||||||
err = c.SetTLSConfig(new)
|
expect = *new
|
||||||
require.Nil(t, err)
|
new.Certificates = nil
|
||||||
|
|
||||||
// The dynamic config should be the one we loaded (with same auther due to nil)
|
// The dynamic config should be the one we loaded (with some different hooks)
|
||||||
require.Equal(t, new, c.TLSConfig(nil))
|
got = c.TLSConfig()
|
||||||
|
require.Equal(expect.Certificates, got.Certificates)
|
||||||
// The server config should return same too for new connections
|
require.Equal(expect.RootCAs, got.RootCAs)
|
||||||
serverCfg = c.ServerTLSConfig()
|
require.Equal(expect.ClientCAs, got.ClientCAs)
|
||||||
require.NotNil(t, serverCfg.GetConfigForClient)
|
require.Equal(expect.InsecureSkipVerify, got.InsecureSkipVerify)
|
||||||
got, err = serverCfg.GetConfigForClient(&tls.ClientHelloInfo{})
|
require.Equal(expect.MinVersion, got.MinVersion)
|
||||||
require.Nil(t, err)
|
require.Equal(expect.CipherSuites, got.CipherSuites)
|
||||||
require.Equal(t, new, got)
|
require.NotNil(got.GetClientCertificate)
|
||||||
|
require.NotNil(got.GetConfigForClient)
|
||||||
|
require.Contains(got.NextProtos, "h2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_verifyServerCertMatchesURI(t *testing.T) {
|
||||||
|
ca1 := connect.TestCA(t, nil)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
certs []*x509.Certificate
|
||||||
|
expected connect.CertURI
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple match",
|
||||||
|
certs: TestPeerCertificates(t, "web", ca1),
|
||||||
|
expected: connect.TestSpiffeIDService(t, "web"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatch",
|
||||||
|
certs: TestPeerCertificates(t, "web", ca1),
|
||||||
|
expected: connect.TestSpiffeIDService(t, "db"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no certs",
|
||||||
|
certs: []*x509.Certificate{},
|
||||||
|
expected: connect.TestSpiffeIDService(t, "db"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil certs",
|
||||||
|
certs: nil,
|
||||||
|
expected: connect.TestSpiffeIDService(t, "db"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := verifyServerCertMatchesURI(tt.certs, tt.expected)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.NotNil(t, err)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue