diff --git a/e2e/etcd_test.go b/e2e/etcd_test.go index 06dd777b0..586b73df8 100644 --- a/e2e/etcd_test.go +++ b/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 } diff --git a/etcdmain/config.go b/etcdmain/config.go index 293db476b..df8ba5e8d 100644 --- a/etcdmain/config.go +++ b/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.") diff --git a/etcdmain/etcd.go b/etcdmain/etcd.go index e6cdf649d..d19557bca 100644 --- a/etcdmain/etcd.go +++ b/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() { diff --git a/pkg/transport/listener.go b/pkg/transport/listener.go index 56c77b409..fcfd435a0 100644 --- a/pkg/transport/listener.go +++ b/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 } diff --git a/pkg/transport/listener_test.go b/pkg/transport/listener_test.go index 48618b648..7dfd59b88 100644 --- a/pkg/transport/listener_test.go +++ b/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) +}