diff --git a/e2e/ctl_v2_test.go b/e2e/ctl_v2_test.go index 382a48013..5e7b8f790 100644 --- a/e2e/ctl_v2_test.go +++ b/e2e/ctl_v2_test.go @@ -276,6 +276,42 @@ func TestCtlV2Backup(t *testing.T) { // For https://github.com/coreos/etcd/issue } } +func TestCtlV2AuthWithCommonName(t *testing.T) { + defer testutil.AfterTest(t) + + copiedCfg := configClientTLS + copiedCfg.clientCertAuthEnabled = true + + epc := setupEtcdctlTest(t, &copiedCfg, false) + defer func() { + if err := epc.Close(); err != nil { + t.Fatalf("error closing etcd processes (%v)", err) + } + }() + + if err := etcdctlRoleAdd(epc, "testrole"); err != nil { + t.Fatalf("failed to add role (%v)", err) + } + if err := etcdctlRoleGrant(epc, "testrole", "--rw", "--path=/foo"); err != nil { + t.Fatalf("failed to grant role (%v)", err) + } + if err := etcdctlUserAdd(epc, "root", "123"); err != nil { + t.Fatalf("failed to add user (%v)", err) + } + if err := etcdctlUserAdd(epc, "Autogenerated CA", "123"); err != nil { + t.Fatalf("failed to add user (%v)", err) + } + if err := etcdctlUserGrant(epc, "Autogenerated CA", "testrole"); err != nil { + t.Fatalf("failed to grant role (%v)", err) + } + if err := etcdctlAuthEnable(epc); err != nil { + t.Fatalf("failed to enable auth (%v)", err) + } + if err := etcdctlSet(epc, "foo", "bar"); err != nil { + t.Fatalf("failed to write (%v)", err) + } +} + func etcdctlPrefixArgs(clus *etcdProcessCluster) []string { endpoints := "" if proxies := clus.proxies(); len(proxies) != 0 { @@ -348,6 +384,13 @@ func etcdctlRoleAdd(clus *etcdProcessCluster, role string) error { return spawnWithExpect(cmdArgs, role) } +func etcdctlRoleGrant(clus *etcdProcessCluster, role string, perms ...string) error { + cmdArgs := append(etcdctlPrefixArgs(clus), "role", "grant") + cmdArgs = append(cmdArgs, perms...) + cmdArgs = append(cmdArgs, role) + return spawnWithExpect(cmdArgs, role) +} + func etcdctlRoleList(clus *etcdProcessCluster, expectedRole string) error { cmdArgs := append(etcdctlPrefixArgs(clus), "role", "list") return spawnWithExpect(cmdArgs, expectedRole) diff --git a/e2e/etcd_test.go b/e2e/etcd_test.go index c7afd3aa7..d96e3e45f 100644 --- a/e2e/etcd_test.go +++ b/e2e/etcd_test.go @@ -149,13 +149,14 @@ type etcdProcessClusterConfig struct { snapCount int // default is 10000 - clientTLS clientConnType - isPeerTLS bool - isPeerAutoTLS bool - isClientAutoTLS bool - forceNewCluster bool - initialToken string - quotaBackendBytes int64 + clientTLS clientConnType + clientCertAuthEnabled bool + isPeerTLS bool + isPeerAutoTLS bool + isClientAutoTLS bool + forceNewCluster bool + initialToken string + quotaBackendBytes int64 } // newEtcdProcessCluster launches a new cluster from etcd processes, returning @@ -325,6 +326,10 @@ func (cfg *etcdProcessClusterConfig) tlsArgs() (args []string) { "--ca-file", caPath, } args = append(args, tlsClientArgs...) + + if cfg.clientCertAuthEnabled { + args = append(args, "--client-cert-auth") + } } } diff --git a/embed/etcd.go b/embed/etcd.go index 6f730aa06..b6f3cc19e 100644 --- a/embed/etcd.go +++ b/embed/etcd.go @@ -116,6 +116,7 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) { QuotaBackendBytes: cfg.QuotaBackendBytes, StrictReconfigCheck: cfg.StrictReconfigCheck, EnablePprof: cfg.EnablePprof, + ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth, } if e.Server, err = etcdserver.NewServer(srvcfg); err != nil { diff --git a/etcdserver/api/v2http/client.go b/etcdserver/api/v2http/client.go index c399e50af..af69b4809 100644 --- a/etcdserver/api/v2http/client.go +++ b/etcdserver/api/v2http/client.go @@ -65,11 +65,12 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http sec := auth.NewStore(server, timeout) kh := &keysHandler{ - sec: sec, - server: server, - cluster: server.Cluster(), - timer: server, - timeout: timeout, + sec: sec, + server: server, + cluster: server.Cluster(), + timer: server, + timeout: timeout, + clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled, } sh := &statsHandler{ @@ -82,6 +83,7 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http cluster: server.Cluster(), timeout: timeout, clock: clockwork.NewRealClock(), + clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled, } dmh := &deprecatedMachinesHandler{ @@ -89,8 +91,9 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http } sech := &authHandler{ - sec: sec, - cluster: server.Cluster(), + sec: sec, + cluster: server.Cluster(), + clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled, } mux := http.NewServeMux() @@ -132,11 +135,12 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http } type keysHandler struct { - sec auth.Store - server etcdserver.Server - cluster api.Cluster - timer etcdserver.RaftTimer - timeout time.Duration + sec auth.Store + server etcdserver.Server + cluster api.Cluster + timer etcdserver.RaftTimer + timeout time.Duration + clientCertAuthEnabled bool } func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -156,7 +160,7 @@ func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } // The path must be valid at this point (we've parsed the request successfully). - if !hasKeyPrefixAccess(h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive) { + if !hasKeyPrefixAccess(h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive, h.clientCertAuthEnabled) { writeKeyNoAuth(w) return } @@ -199,18 +203,19 @@ func (h *deprecatedMachinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } type membersHandler struct { - sec auth.Store - server etcdserver.Server - cluster api.Cluster - timeout time.Duration - clock clockwork.Clock + sec auth.Store + server etcdserver.Server + cluster api.Cluster + timeout time.Duration + clock clockwork.Clock + clientCertAuthEnabled bool } func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") { return } - if !hasWriteRootAccess(h.sec, r) { + if !hasWriteRootAccess(h.sec, r, h.clientCertAuthEnabled) { writeNoAuth(w, r) return } diff --git a/etcdserver/api/v2http/client_auth.go b/etcdserver/api/v2http/client_auth.go index cf1585bed..2b3278528 100644 --- a/etcdserver/api/v2http/client_auth.go +++ b/etcdserver/api/v2http/client_auth.go @@ -26,18 +26,56 @@ import ( ) type authHandler struct { - sec auth.Store - cluster api.Cluster + sec auth.Store + cluster api.Cluster + clientCertAuthEnabled bool } -func hasWriteRootAccess(sec auth.Store, r *http.Request) bool { +func hasWriteRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool { if r.Method == "GET" || r.Method == "HEAD" { return true } - return hasRootAccess(sec, r) + return hasRootAccess(sec, r, clientCertAuthEnabled) } -func hasRootAccess(sec auth.Store, r *http.Request) bool { +func userFromBasicAuth(sec auth.Store, r *http.Request) *auth.User { + username, password, ok := r.BasicAuth() + if !ok { + plog.Warningf("auth: malformed basic auth encoding") + return nil + } + user, err := sec.GetUser(username) + if err != nil { + return nil + } + + ok = sec.CheckPassword(user, password) + if !ok { + plog.Warningf("auth: incorrect password for user: %s", username) + return nil + } + return &user +} + +func userFromClientCertificate(sec auth.Store, r *http.Request) *auth.User { + if r.TLS == nil { + return nil + } + + for _, chains := range r.TLS.VerifiedChains { + for _, chain := range chains { + plog.Debugf("auth: found common name %s.\n", chain.Subject.CommonName) + user, err := sec.GetUser(chain.Subject.CommonName) + if err == nil { + plog.Debugf("auth: authenticated user %s by cert common name.", user.User) + return &user + } + } + } + return nil +} + +func hasRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool { if sec == nil { // No store means no auth available, eg, tests. return true @@ -45,30 +83,30 @@ func hasRootAccess(sec auth.Store, r *http.Request) bool { if !sec.AuthEnabled() { return true } - username, password, ok := r.BasicAuth() - if !ok { - return false - } - rootUser, err := sec.GetUser(username) - if err != nil { - return false + + var rootUser *auth.User + if r.Header.Get("Authorization") == "" && clientCertAuthEnabled { + rootUser = userFromClientCertificate(sec, r) + if rootUser == nil { + return false + } + } else { + rootUser = userFromBasicAuth(sec, r) + if rootUser == nil { + return false + } } - ok = sec.CheckPassword(rootUser, password) - if !ok { - plog.Warningf("auth: wrong password for user %s", username) - return false - } for _, role := range rootUser.Roles { if role == auth.RootRoleName { return true } } - plog.Warningf("auth: user %s does not have the %s role for resource %s.", username, auth.RootRoleName, r.URL.Path) + plog.Warningf("auth: user %s does not have the %s role for resource %s.", rootUser.User, auth.RootRoleName, r.URL.Path) return false } -func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive bool) bool { +func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive, clientCertAuthEnabled bool) bool { if sec == nil { // No store means no auth available, eg, tests. return true @@ -76,25 +114,21 @@ func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive b if !sec.AuthEnabled() { return true } - if r.Header.Get("Authorization") == "" { - plog.Warningf("auth: no authorization provided, checking guest access") - return hasGuestAccess(sec, r, key) - } - username, password, ok := r.BasicAuth() - if !ok { - plog.Warningf("auth: malformed basic auth encoding") - return false - } - user, err := sec.GetUser(username) - if err != nil { - plog.Warningf("auth: no such user: %s.", username) - return false - } - authAsUser := sec.CheckPassword(user, password) - if !authAsUser { - plog.Warningf("auth: incorrect password for user: %s.", username) - return false + + var user *auth.User + if r.Header.Get("Authorization") == "" && clientCertAuthEnabled { + user = userFromClientCertificate(sec, r) + if user == nil { + plog.Warningf("auth: no authorization provided, checking guest access") + return hasGuestAccess(sec, r, key) + } + } else { + user = userFromBasicAuth(sec, r) + if user == nil { + return false + } } + writeAccess := r.Method != "GET" && r.Method != "HEAD" for _, roleName := range user.Roles { role, err := sec.GetRole(roleName) @@ -109,7 +143,7 @@ func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive b return true } } - plog.Warningf("auth: invalid access for user %s on key %s.", username, key) + plog.Warningf("auth: invalid access for user %s on key %s.", user.User, key) return false } @@ -145,7 +179,7 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) { if !allowMethod(w, r.Method, "GET") { return } - if !hasRootAccess(sh.sec, r) { + if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) { writeNoAuth(w, r) return } @@ -209,7 +243,7 @@ func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role stri if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { return } - if !hasRootAccess(sh.sec, r) { + if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) { writeNoAuth(w, r) return } @@ -293,7 +327,7 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) { if !allowMethod(w, r.Method, "GET") { return } - if !hasRootAccess(sh.sec, r) { + if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) { writeNoAuth(w, r) return } @@ -365,7 +399,7 @@ func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user stri if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { return } - if !hasRootAccess(sh.sec, r) { + if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) { writeNoAuth(w, r) return } @@ -478,7 +512,7 @@ func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) { if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { return } - if !hasWriteRootAccess(sh.sec, r) { + if !hasWriteRootAccess(sh.sec, r, sh.clientCertAuthEnabled) { writeNoAuth(w, r) return } diff --git a/etcdserver/api/v2http/client_auth_test.go b/etcdserver/api/v2http/client_auth_test.go index 734e19ac0..b5e32c487 100644 --- a/etcdserver/api/v2http/client_auth_test.go +++ b/etcdserver/api/v2http/client_auth_test.go @@ -15,9 +15,13 @@ package v2http import ( + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -440,6 +444,32 @@ func mustAuthRequest(method, username, password string) *http.Request { return req } +func unauthedRequest(method string) *http.Request { + req, err := http.NewRequest(method, "path", strings.NewReader("")) + if err != nil { + panic("Cannot make request: " + err.Error()) + } + return req +} + +func tlsAuthedRequest(req *http.Request, certname string) *http.Request { + bytes, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.pem", certname)) + if err != nil { + panic(err) + } + + block, _ := pem.Decode(bytes) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(err) + } + + req.TLS = &tls.ConnectionState{ + VerifiedChains: [][]*x509.Certificate{{cert}}, + } + return req +} + func TestPrefixAccess(t *testing.T) { var table = []struct { key string @@ -690,14 +720,161 @@ func TestPrefixAccess(t *testing.T) { } for i, tt := range table { - if tt.hasRoot != hasRootAccess(tt.store, tt.req) { + if tt.hasRoot != hasRootAccess(tt.store, tt.req, true) { t.Errorf("#%d: hasRoot doesn't match (expected %v)", i, tt.hasRoot) } - if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, false) { + if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, false, true) { t.Errorf("#%d: hasKeyPrefixAccess doesn't match (expected %v)", i, tt.hasRoot) } - if tt.hasRecursiveAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, true) { + if tt.hasRecursiveAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, true, true) { t.Errorf("#%d: hasRecursiveAccess doesn't match (expected %v)", i, tt.hasRoot) } } } + +func TestUserFromClientCertificate(t *testing.T) { + witherror := &mockAuthStore{ + users: map[string]*auth.User{ + "user": { + User: "user", + Roles: []string{"root"}, + Password: "password", + }, + "basicauth": { + User: "basicauth", + Roles: []string{"root"}, + Password: "password", + }, + }, + roles: map[string]*auth.Role{ + "root": { + Role: "root", + }, + }, + err: errors.New(""), + } + + noerror := &mockAuthStore{ + users: map[string]*auth.User{ + "user": { + User: "user", + Roles: []string{"root"}, + Password: "password", + }, + "basicauth": { + User: "basicauth", + Roles: []string{"root"}, + Password: "password", + }, + }, + roles: map[string]*auth.Role{ + "root": { + Role: "root", + }, + }, + } + + var table = []struct { + req *http.Request + userExists bool + store auth.Store + username string + }{ + { + // non tls request + req: unauthedRequest("GET"), + userExists: false, + store: witherror, + }, + { + // cert with cn of existing user + req: tlsAuthedRequest(unauthedRequest("GET"), "user"), + userExists: true, + username: "user", + store: noerror, + }, + { + // cert with cn of non-existing user + req: tlsAuthedRequest(unauthedRequest("GET"), "otheruser"), + userExists: false, + store: witherror, + }, + } + + for i, tt := range table { + user := userFromClientCertificate(tt.store, tt.req) + userExists := user != nil + + if tt.userExists != userExists { + t.Errorf("#%d: userFromClientCertificate doesn't match (expected %v)", i, tt.userExists) + } + if user != nil && (tt.username != user.User) { + t.Errorf("#%d: userFromClientCertificate username doesn't match (expected %s, got %s)", i, tt.username, user.User) + } + } +} + +func TestUserFromBasicAuth(t *testing.T) { + sec := &mockAuthStore{ + users: map[string]*auth.User{ + "user": { + User: "user", + Roles: []string{"root"}, + Password: "password", + }, + }, + roles: map[string]*auth.Role{ + "root": { + Role: "root", + }, + }, + } + + var table = []struct { + username string + req *http.Request + userExists bool + }{ + { + // valid user, valid pass + username: "user", + req: mustAuthRequest("GET", "user", "password"), + userExists: true, + }, + { + // valid user, bad pass + username: "user", + req: mustAuthRequest("GET", "user", "badpass"), + userExists: false, + }, + { + // valid user, no pass + username: "user", + req: mustAuthRequest("GET", "user", ""), + userExists: false, + }, + { + // missing user + username: "missing", + req: mustAuthRequest("GET", "missing", "badpass"), + userExists: false, + }, + { + // no basic auth + req: unauthedRequest("GET"), + userExists: false, + }, + } + + for i, tt := range table { + user := userFromBasicAuth(sec, tt.req) + userExists := user != nil + + if tt.userExists != userExists { + t.Errorf("#%d: userFromBasicAuth doesn't match (expected %v)", i, tt.userExists) + } + if user != nil && (tt.username != user.User) { + t.Errorf("#%d: userFromBasicAuth username doesn't match (expected %s, got %s)", i, tt.username, user.User) + } + } +} diff --git a/etcdserver/api/v2http/testdata/ca.pem b/etcdserver/api/v2http/testdata/ca.pem new file mode 100644 index 000000000..60cbee3bb --- /dev/null +++ b/etcdserver/api/v2http/testdata/ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEjCCAfqgAwIBAgIIYpX+8HgWGfkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE +AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA1MDBaFw0yMDExMjIwMzA1MDBaMBUx +EzARBgNVBAMTCmV0Y2QgdGVzdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDa9PkwEwiBD8mB+VIKz5r5gRHnNF4Icj6T6R/RsdatecQe6vU0EU4FXtKZ +drWnCGlATyrQooqHpb+rDc7CUt3mXrIxrNkcGTMaesF7P0GWxVkyOGSjJMxGBv3e +bAZknBe4eLMi68L1aT/uYmxcp/B3L2mfdFtc1Gd6mYJpNm1PgilRyIrO0mY5ysIX +4WHCa3yudAv8HrFbQcw7l7OyKA6uSWg6h07lE3d5jw5YOly+hz0iaRtzhb4tJrYD +Lm1tehb0nnoLuW6yYblRSoyBVDT50MFVlyvW40Po5WkOXw/wnsnyxWRR4yqU23wq +quQU0HXJEBLFnT+KbLOQ0EAE35vXAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIBBjAS +BgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBSbUCGB95ochDrbEZlzGGYuA7xu +xjAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlzGGYuA7xuxjANBgkqhkiG9w0BAQsF +AAOCAQEAardO/SGCu7Snz3YRBUinzpZEUFTFend+FJtBkxBXCao1RvTXg8PBMkza +LUsaR4mLsGoXLIbNCoIinvVG0QULYCZe11N3l1L0G2g5uhEM4MfJ2rwrMD0o17i+ +nwNRRE3tfKAlWhYQg+4ye36kQVxASPniHjdQgjKYUFTNXdyG6DzuAclaVte9iVw6 +cWl61fB2CZya3+uMtih8t/Kgl2KbMO2PvNByfnDjKmW+v58qHbXyoJZqnpvDn14+ +p2Ox+AvvxYiEiUIvFdWy101QB7NJMCtdwq6oG6OvIOgXzLgitTFSq4kfWDfupQjW +iFoQ+vWmYhK5ld0nBaiz+JmHuemK7A== +-----END CERTIFICATE----- diff --git a/etcdserver/api/v2http/testdata/otheruser.pem b/etcdserver/api/v2http/testdata/otheruser.pem new file mode 100644 index 000000000..d0c74eb9f --- /dev/null +++ b/etcdserver/api/v2http/testdata/otheruser.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIINYpsso1f3SswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE +AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMBQx +EjAQBgNVBAMTCW90aGVydXNlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAPOAUa5GblwIjHTEnox2c/Am9jV1TMvzBuVXxnp2UnNHMNwstAooFrEs/Z+d +ft5AOsooP6zVuM3eBQa4i9huJbVNDfPU2H94yA89jYfJYUgo7C838V6NjGsCCptQ +WzkKPNlDbT9xA/7XpIUJ2WltuYDRrjWq8pXQONqTjcg5n4l0JO8xdHJHRUkFQ76F +1npXeLndgGaP11lqzpYlglEGi5URhzAT1xxQ0hLSe8WNmiCxxkq++C8Gx4sPg9mX +M94aoJDzZSnoaqDxckbP/7Q0ZKe/fVdCFkd5+jqT4Mt7hwmz9jTCHcVnAz4EKI+t +rbWgbCfMK6013GotXz7InStVe+MCAwEAAaOBjTCBijAOBgNVHQ8BAf8EBAMCBaAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD +VR0OBBYEFFwMmf+pnaejmri6y1T+lfU+MBq/MB8GA1UdIwQYMBaAFJtQIYH3mhyE +OtsRmXMYZi4DvG7GMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEACOn6 +mec29MTMGPt/EPOmSyhvTKSwH+5YWjCbyUFeoB8puxrJlIphK4mvT+sXp2wzno89 +FVCliO/rJurdErKvyOjlK1QrVGPYIt7Wz9ssAfvlwCyBM8PqgEG8dJN9aAkf2h4r +Ye+hBh1y6Nnataf7lxe9mqAOvD/7wVIgzjCnMD1q5QSY2Mln3HwVQXtbZFbY363Z +X9Fk3PUpjJSX9jbEz9kIlT8AJAdxl6GB8Z9B8PrA8qf4Bhk15ICRHxb67EhDrGWV +8q7ArU2XBqs/+GWpUIMoGKNZv+K+/SksZK1KnzaUvApUCJzt+ac+p8HOgMdvDRgr +GfVVJqcZgyEmeczy0A== +-----END CERTIFICATE----- diff --git a/etcdserver/api/v2http/testdata/user.pem b/etcdserver/api/v2http/testdata/user.pem new file mode 100644 index 000000000..0fc210865 --- /dev/null +++ b/etcdserver/api/v2http/testdata/user.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNDCCAhygAwIBAgIIcQ0DAfgevocwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE +AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMA8x +DTALBgNVBAMTBHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0 ++3Lm1SmUJJLufaFTYz+e5qyQEshNRyeAhXIeZ1aw+yBjslXGZQ3/uGOwnOnGqUeA +Nidc9ty4NsK6RVppHlezUrBnpl4hws8vHWFKZpU2R6kKL8EYLmg+iVqEBj7XqfAp +8bJqqZI3KOqLXpRH55mA69KP7VEK9ngTVR/tERSrUPT8jcjwbvhSOqD8Qk07BUDR +6RpDr94Mnaf+fMGG36Sh7iUl+i4Oh6FFar+7+b0+5Bhs2/6uVsK4A1Z3jqqfSQH8 +q8Wf5h9Ka4aqGSw4ia5G3Uw7Jsl2aDgpJ7uwJo1k8SclbMYnYdhZuo+U+esY/Fai +YdbjG+AroZ+y9TB8bMlHAgMBAAGjgY0wgYowDgYDVR0PAQH/BAQDAgWgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW +BBRuTt0lJIVKYaz76aSxl/MQOLRwfDAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlz +GGYuA7xuxjALBgNVHREEBDACggAwDQYJKoZIhvcNAQELBQADggEBABLRWZm+Lgjs +c5qDXbgOJW2pR630syY8ixR9c6HvzPVJim8mFioMX+xrlbOC6BmOUlOb9j83bTKn +aOg/0xlpxNbd8QYzgRxZmHZLULPdiNeeRvIzsrzrH88+inrmZhRXRVcHjdO6CG6t +hCdDdRiNU6GkF7dPna0xNcEOKe2wUfzd1ZtKOqzi1w+fKjSeMplZomeWgP4WRvkh +JJ/0ujlMMckgyTxRh8EEaJ35OnpXX7EdipoWhOMmiUnlPqye2icC8Y+CMdZsrod6 +nkoEQnXDCLf/Iv0qj7B9iKbxn7t3QDVxY4UILUReDuD8yrGULlGOl//aY/T3pkZ6 +R5trduZhI3o= +-----END CERTIFICATE----- diff --git a/etcdserver/config.go b/etcdserver/config.go index f7cfe8634..b66a63323 100644 --- a/etcdserver/config.go +++ b/etcdserver/config.go @@ -56,6 +56,9 @@ type ServerConfig struct { StrictReconfigCheck bool EnablePprof bool + + // ClientCertAuthEnabled is true when cert has been signed by the client CA. + ClientCertAuthEnabled bool } // VerifyBootstrap sanity-checks the initial config for bootstrap case