Browse Source

Merge pull request #4484 from heyitsanthony/auto-tls

automatic peer TLS
Anthony Romano 9 years ago
parent
commit
3fed78ae7b
5 changed files with 144 additions and 10 deletions
  1. 22 10
      e2e/etcd_test.go
  2. 2 0
      etcdmain/config.go
  3. 14 0
      etcdmain/etcd.go
  4. 85 0
      pkg/transport/listener.go
  5. 21 0
      pkg/transport/listener_test.go

+ 22 - 10
e2e/etcd_test.go

@@ -43,6 +43,12 @@ var (
 		isPeerTLS:    false,
 		initialToken: "new",
 	}
+	configAutoTLS = etcdProcessClusterConfig{
+		clusterSize:   3,
+		isPeerTLS:     true,
+		isPeerAutoTLS: true,
+		initialToken:  "new",
+	}
 	configTLS = etcdProcessClusterConfig{
 		clusterSize:  3,
 		proxySize:    0,
@@ -94,6 +100,7 @@ func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {
 }
 
 func TestBasicOpsNoTLS(t *testing.T)        { testBasicOpsPutGet(t, &configNoTLS) }
+func TestBasicOpsAutoTLS(t *testing.T)      { testBasicOpsPutGet(t, &configAutoTLS) }
 func TestBasicOpsAllTLS(t *testing.T)       { testBasicOpsPutGet(t, &configTLS) }
 func TestBasicOpsPeerTLS(t *testing.T)      { testBasicOpsPutGet(t, &configPeerTLS) }
 func TestBasicOpsClientTLS(t *testing.T)    { testBasicOpsPutGet(t, &configClientTLS) }
@@ -170,11 +177,12 @@ type etcdProcessConfig struct {
 }
 
 type etcdProcessClusterConfig struct {
-	clusterSize  int
-	proxySize    int
-	isClientTLS  bool
-	isPeerTLS    bool
-	initialToken string
+	clusterSize   int
+	proxySize     int
+	isClientTLS   bool
+	isPeerTLS     bool
+	isPeerAutoTLS bool
+	initialToken  string
 }
 
 // newEtcdProcessCluster launches a new cluster from etcd processes, returning
@@ -325,12 +333,16 @@ func (cfg *etcdProcessClusterConfig) tlsArgs() (args []string) {
 		args = append(args, tlsClientArgs...)
 	}
 	if cfg.isPeerTLS {
-		tlsPeerArgs := []string{
-			"--peer-cert-file", certPath,
-			"--peer-key-file", privateKeyPath,
-			"--peer-ca-file", caPath,
+		if cfg.isPeerAutoTLS {
+			args = append(args, "--peer-auto-tls=true")
+		} else {
+			tlsPeerArgs := []string{
+				"--peer-cert-file", certPath,
+				"--peer-key-file", privateKeyPath,
+				"--peer-ca-file", caPath,
+			}
+			args = append(args, tlsPeerArgs...)
 		}
-		args = append(args, tlsPeerArgs...)
 	}
 	return args
 }

+ 2 - 0
etcdmain/config.go

@@ -111,6 +111,7 @@ type config struct {
 
 	// security
 	clientTLSInfo, peerTLSInfo transport.TLSInfo
+	peerAutoTLS                bool
 
 	// logging
 	debug        bool
@@ -211,6 +212,7 @@ func NewConfig() *config {
 	fs.StringVar(&cfg.peerTLSInfo.KeyFile, "peer-key-file", "", "Path to the peer server TLS key file.")
 	fs.BoolVar(&cfg.peerTLSInfo.ClientCertAuth, "peer-client-cert-auth", false, "Enable peer client cert authentication.")
 	fs.StringVar(&cfg.peerTLSInfo.TrustedCAFile, "peer-trusted-ca-file", "", "Path to the peer server TLS trusted CA file.")
+	fs.BoolVar(&cfg.peerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates")
 
 	// logging
 	fs.BoolVar(&cfg.debug, "debug", false, "Enable debug-level logging for etcd.")

+ 14 - 0
etcdmain/etcd.go

@@ -203,9 +203,23 @@ func startEtcd(cfg *config) (<-chan struct{}, error) {
 		return nil, fmt.Errorf("error setting up initial cluster: %v", err)
 	}
 
+	if cfg.peerAutoTLS && cfg.peerTLSInfo.Empty() {
+		phosts := make([]string, 0)
+		for _, u := range cfg.lpurls {
+			phosts = append(phosts, u.Host)
+		}
+		cfg.peerTLSInfo, err = transport.SelfCert(cfg.dir, phosts)
+		if err != nil {
+			plog.Fatalf("could not get certs (%v)", err)
+		}
+	} else if cfg.peerAutoTLS {
+		plog.Warningf("ignoring peer auto TLS since certs given")
+	}
+
 	if !cfg.peerTLSInfo.Empty() {
 		plog.Infof("peerTLS: %s", cfg.peerTLSInfo)
 	}
+
 	plns := make([]net.Listener, 0)
 	for _, u := range cfg.lpurls {
 		if u.Scheme == "http" && !cfg.peerTLSInfo.Empty() {

+ 85 - 0
pkg/transport/listener.go

@@ -15,13 +15,21 @@
 package transport
 
 import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
 	"crypto/tls"
 	"crypto/x509"
+	"crypto/x509/pkix"
 	"encoding/pem"
 	"fmt"
 	"io/ioutil"
+	"math/big"
 	"net"
 	"net/http"
+	"os"
+	"path"
+	"strings"
 	"time"
 )
 
@@ -79,6 +87,8 @@ type TLSInfo struct {
 	TrustedCAFile  string
 	ClientCertAuth bool
 
+	selfCert bool
+
 	// parseFunc exists to simplify testing. Typically, parseFunc
 	// should be left nil. In that case, tls.X509KeyPair will be used.
 	parseFunc func([]byte, []byte) (tls.Certificate, error)
@@ -92,6 +102,78 @@ func (info TLSInfo) Empty() bool {
 	return info.CertFile == "" && info.KeyFile == ""
 }
 
+func SelfCert(dirpath string, hosts []string) (info TLSInfo, err error) {
+	if err = os.MkdirAll(dirpath, 0700); err != nil {
+		return
+	}
+
+	certPath := path.Join(dirpath, "cert.pem")
+	keyPath := path.Join(dirpath, "key.pem")
+	_, errcert := os.Stat(certPath)
+	_, errkey := os.Stat(keyPath)
+	if errcert == nil && errkey == nil {
+		info.CertFile = certPath
+		info.KeyFile = keyPath
+		info.selfCert = true
+		return
+	}
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return
+	}
+
+	tmpl := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject:      pkix.Name{Organization: []string{"etcd"}},
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().Add(365 * (24 * time.Hour)),
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+	}
+
+	for _, host := range hosts {
+		if ip := net.ParseIP(host); ip != nil {
+			tmpl.IPAddresses = append(tmpl.IPAddresses, ip)
+		} else {
+			tmpl.DNSNames = append(tmpl.DNSNames, strings.Split(host, ":")[0])
+		}
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	if err != nil {
+		return
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
+	if err != nil {
+		return
+	}
+
+	certOut, err := os.Create(certPath)
+	if err != nil {
+		return
+	}
+	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+	certOut.Close()
+
+	b, err := x509.MarshalECPrivateKey(priv)
+	if err != nil {
+		return
+	}
+	keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return
+	}
+	pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
+	keyOut.Close()
+
+	return SelfCert(dirpath, hosts)
+}
+
 func (info TLSInfo) baseConfig() (*tls.Config, error) {
 	if info.KeyFile == "" || info.CertFile == "" {
 		return nil, fmt.Errorf("KeyFile and CertFile must both be present[key: %v, cert: %v]", info.KeyFile, info.CertFile)
@@ -182,6 +264,9 @@ func (info TLSInfo) ClientConfig() (*tls.Config, error) {
 		}
 	}
 
+	if info.selfCert {
+		cfg.InsecureSkipVerify = true
+	}
 	return cfg, nil
 }
 

+ 21 - 0
pkg/transport/listener_test.go

@@ -54,6 +54,10 @@ func TestNewListenerTLSInfo(t *testing.T) {
 	defer os.Remove(tmp)
 	tlsInfo := TLSInfo{CertFile: tmp, KeyFile: tmp}
 	tlsInfo.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil)
+	testNewListenerTLSInfoAccept(t, tlsInfo)
+}
+
+func testNewListenerTLSInfoAccept(t *testing.T, tlsInfo TLSInfo) {
 	ln, err := NewListener("127.0.0.1:0", "https", tlsInfo)
 	if err != nil {
 		t.Fatalf("unexpected NewListener error: %v", err)
@@ -249,3 +253,20 @@ func TestNewListenerUnixSocket(t *testing.T) {
 	}
 	l.Close()
 }
+
+// TestNewListenerTLSInfoSelfCert tests that a new certificate accepts connections.
+func TestNewListenerTLSInfoSelfCert(t *testing.T) {
+	tmpdir, err := ioutil.TempDir(os.TempDir(), "tlsdir")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpdir)
+	tlsinfo, err := SelfCert(tmpdir, []string{"127.0.0.1"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if tlsinfo.Empty() {
+		t.Fatalf("tlsinfo should have certs (%+v)", tlsinfo)
+	}
+	testNewListenerTLSInfoAccept(t, tlsinfo)
+}