From 0191509637546621d6f2e18e074e955ab8ef374d Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Mon, 21 Nov 2016 13:47:29 +0900 Subject: [PATCH 1/2] auth, etcdserver: authenticate clients based on certificate CommonName This commit lets v3 auth mechanism authenticate clients based on CommonName of certificate like v2 auth. --- auth/store.go | 27 +++++++++++++++++++++++++++ etcdserver/api/v3rpc/maintenance.go | 3 ++- etcdserver/server.go | 2 +- etcdserver/v3_server.go | 15 +++++++++++++-- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/auth/store.go b/auth/store.go index 8037d6d0c..6b2c6c923 100644 --- a/auth/store.go +++ b/auth/store.go @@ -30,7 +30,9 @@ import ( "github.com/coreos/pkg/capnslog" "golang.org/x/crypto/bcrypt" "golang.org/x/net/context" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" ) var ( @@ -159,6 +161,9 @@ type AuthStore interface { // AuthInfoFromCtx gets AuthInfo from gRPC's context AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) + + // AuthInfoFromTLS gets AuthInfo from TLS info of gRPC's context + AuthInfoFromTLS(ctx context.Context) *AuthInfo } type authStore struct { @@ -950,6 +955,28 @@ func (as *authStore) isValidSimpleToken(token string, ctx context.Context) bool return false } +func (as *authStore) AuthInfoFromTLS(ctx context.Context) *AuthInfo { + peer, ok := peer.FromContext(ctx) + if !ok || peer == nil || peer.AuthInfo == nil { + return nil + } + + tlsInfo := peer.AuthInfo.(credentials.TLSInfo) + for _, chains := range tlsInfo.State.VerifiedChains { + for _, chain := range chains { + cn := chain.Subject.CommonName + plog.Debugf("found common name %s", cn) + + return &AuthInfo{ + Username: cn, + Revision: as.Revision(), + } + } + } + + return nil +} + func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) { md, ok := metadata.FromContext(ctx) if !ok { diff --git a/etcdserver/api/v3rpc/maintenance.go b/etcdserver/api/v3rpc/maintenance.go index a59e6f9a0..50d9abc90 100644 --- a/etcdserver/api/v3rpc/maintenance.go +++ b/etcdserver/api/v3rpc/maintenance.go @@ -47,6 +47,7 @@ type RaftStatusGetter interface { } type AuthGetter interface { + AuthInfoFromCtx(ctx context.Context) (*auth.AuthInfo, error) AuthStore() auth.AuthStore } @@ -152,7 +153,7 @@ type authMaintenanceServer struct { } func (ams *authMaintenanceServer) isAuthenticated(ctx context.Context) error { - authInfo, err := ams.ag.AuthStore().AuthInfoFromCtx(ctx) + authInfo, err := ams.ag.AuthInfoFromCtx(ctx) if err != nil { return err } diff --git a/etcdserver/server.go b/etcdserver/server.go index 8afdc95f4..76f5ff616 100644 --- a/etcdserver/server.go +++ b/etcdserver/server.go @@ -1022,7 +1022,7 @@ func (s *EtcdServer) checkMembershipOperationPermission(ctx context.Context) err // in the state machine layer // However, both of membership change and role management requires the root privilege. // So careful operation by admins can prevent the problem. - authInfo, err := s.AuthStore().AuthInfoFromCtx(ctx) + authInfo, err := s.AuthInfoFromCtx(ctx) if err != nil { return err } diff --git a/etcdserver/v3_server.go b/etcdserver/v3_server.go index 60653cb6d..78a4c0384 100644 --- a/etcdserver/v3_server.go +++ b/etcdserver/v3_server.go @@ -617,7 +617,7 @@ func (s *EtcdServer) RoleDelete(ctx context.Context, r *pb.AuthRoleDeleteRequest // doSerialize handles the auth logic, with permissions checked by "chk", for a serialized request "get". Returns a non-nil error on authentication failure. func (s *EtcdServer) doSerialize(ctx context.Context, chk func(*auth.AuthInfo) error, get func()) error { for { - ai, err := s.AuthStore().AuthInfoFromCtx(ctx) + ai, err := s.AuthInfoFromCtx(ctx) if err != nil { return err } @@ -652,7 +652,7 @@ func (s *EtcdServer) processInternalRaftRequestOnce(ctx context.Context, r pb.In ID: s.reqIDGen.Next(), } - authInfo, err := s.AuthStore().AuthInfoFromCtx(ctx) + authInfo, err := s.AuthInfoFromCtx(ctx) if err != nil { return nil, err } @@ -802,3 +802,14 @@ func (s *EtcdServer) linearizableReadNotify(ctx context.Context) error { return ErrStopped } } + +func (s *EtcdServer) AuthInfoFromCtx(ctx context.Context) (*auth.AuthInfo, error) { + if s.Cfg.ClientCertAuthEnabled { + authInfo := s.AuthStore().AuthInfoFromTLS(ctx) + if authInfo != nil { + return authInfo, nil + } + } + + return s.AuthStore().AuthInfoFromCtx(ctx) +} From cd9f0a1721eee9c937d1252b8d6233883415fec7 Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Mon, 21 Nov 2016 16:42:46 +0900 Subject: [PATCH 2/2] e2e: add a case for CommonName auth of v3 API --- e2e/ctl_v3_auth_test.go | 34 ++++++++++++++++++++++++++++++++++ e2e/etcd_test.go | 7 +++++++ 2 files changed, 41 insertions(+) diff --git a/e2e/ctl_v3_auth_test.go b/e2e/ctl_v3_auth_test.go index 018d3447c..a58ea95cf 100644 --- a/e2e/ctl_v3_auth_test.go +++ b/e2e/ctl_v3_auth_test.go @@ -34,6 +34,7 @@ func TestCtlV3AuthMemberRemove(t *testing.T) { testCtl(t, authTestMemberRemove, withQuorum(), withNoStrictReconfig()) } func TestCtlV3AuthMemberUpdate(t *testing.T) { testCtl(t, authTestMemberUpdate) } +func TestCtlV3AuthCertCN(t *testing.T) { testCtl(t, authTestCertCN, withCfg(configClientTLSCertAuth)) } func authEnableTest(cx ctlCtx) { if err := authEnable(cx); err != nil { @@ -549,3 +550,36 @@ func authTestMemberUpdate(cx ctlCtx) { cx.t.Fatal(err) } } + +func authTestCertCN(cx ctlCtx) { + if err := ctlV3User(cx, []string{"add", "etcd", "--interactive=false"}, "User etcd created", []string{""}); err != nil { + cx.t.Fatal(err) + } + if err := spawnWithExpect(append(cx.PrefixArgs(), "role", "add", "test-role"), "Role test-role created"); err != nil { + cx.t.Fatal(err) + } + if err := ctlV3User(cx, []string{"grant-role", "etcd", "test-role"}, "Role test-role is granted to user etcd", nil); err != nil { + cx.t.Fatal(err) + } + cmd := append(cx.PrefixArgs(), "role", "grant-permission", "test-role", "readwrite", "foo") + if err := spawnWithExpect(cmd, "Role test-role updated"); err != nil { + cx.t.Fatal(err) + } + + // grant a new key + if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "hoo", "", false}); err != nil { + cx.t.Fatal(err) + } + + // try a granted key + cx.user, cx.pass = "", "" + if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { + cx.t.Fatal(err) + } + + // try a non granted key + cx.user, cx.pass = "", "" + if err := ctlV3PutFailPerm(cx, "baz", "bar"); err == nil { + cx.t.Fatal(err) + } +} diff --git a/e2e/etcd_test.go b/e2e/etcd_test.go index c7065d382..bebc52f3c 100644 --- a/e2e/etcd_test.go +++ b/e2e/etcd_test.go @@ -106,6 +106,13 @@ var ( isPeerTLS: true, initialToken: "new", } + configClientTLSCertAuth = etcdProcessClusterConfig{ + clusterSize: 1, + proxySize: 0, + clientTLS: clientTLS, + initialToken: "new", + clientCertAuthEnabled: true, + } ) func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {