Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2http: client certificate auth via common name #5991

Merged
merged 9 commits into from
Jul 21, 2016
43 changes: 43 additions & 0 deletions e2e/ctl_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 12 additions & 7 deletions e2e/etcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions embed/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 24 additions & 19 deletions etcdserver/api/v2http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -82,15 +83,17 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http
cluster: server.Cluster(),
timeout: timeout,
clock: clockwork.NewRealClock(),
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
}

dmh := &deprecatedMachinesHandler{
cluster: server.Cluster(),
}

sech := &authHandler{
sec: sec,
cluster: server.Cluster(),
sec: sec,
cluster: server.Cluster(),
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
}

mux := http.NewServeMux()
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
120 changes: 77 additions & 43 deletions etcdserver/api/v2http/client_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,75 +26,109 @@ 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
}
if !sec.AuthEnabled() {
return true
}
username, password, ok := r.BasicAuth()
if !ok {
return false
}
rootUser, err := sec.GetUser(username)
if err != nil {
return false
}

ok = sec.CheckPassword(rootUser, password)
if !ok {
plog.Warningf("auth: wrong password for user %s", username)
return false
var rootUser *auth.User
if r.Header.Get("Authorization") == "" && clientCertAuthEnabled {
rootUser = userFromClientCertificate(sec, r)
Copy link
Contributor

@xiang90 xiang90 Jul 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably should only enable this checking when client auth is set: https://github.com/coreos/etcd/blob/master/etcdmain/config.go#L174

Or we trust random client certs.

/cc @gtank @heyitsanthony any opinion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it should ensure the cert's been signed by the client ca

if rootUser == nil {
return false
}
} else {
rootUser = userFromBasicAuth(sec, r)
if rootUser == nil {
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
}
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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading