diff --git a/go/cmd/vtgate/plugin_auth_clientcert.go b/go/cmd/vtgate/plugin_auth_clientcert.go new file mode 100644 index 0000000000..8f6dd8ac66 --- /dev/null +++ b/go/cmd/vtgate/plugin_auth_clientcert.go @@ -0,0 +1,12 @@ +package main + +// This plugin imports clientcert to register the client certificate implementation of AuthServer. + +import ( + "vitess.io/vitess/go/mysql/clientcert" + "vitess.io/vitess/go/vt/vtgate" +) + +func init() { + vtgate.RegisterPluginInitializer(func() { clientcert.Init() }) +} diff --git a/go/mysql/clientcert/auth_server_clientcert.go b/go/mysql/clientcert/auth_server_clientcert.go new file mode 100644 index 0000000000..ae8e2a582d --- /dev/null +++ b/go/mysql/clientcert/auth_server_clientcert.go @@ -0,0 +1,73 @@ +package clientcert + +import ( + "flag" + "fmt" + "net" + "vitess.io/vitess/go/mysql" + "vitess.io/vitess/go/vt/log" + querypb "vitess.io/vitess/go/vt/proto/query" +) + +var clientcertAuthMethod = flag.String("mysql_clientcert_auth_method", mysql.MysqlClearPassword, "client-side authentication method to use. Supported values: mysql_clear_password, dialog.") + +type AuthServerClientCert struct { + Method string +} + +type UserData struct { + querypb.VTGateCallerID +} + +// Init is public so it can be called from plugin_auth_ldap.go (go/cmd/vtgate) +func Init() { + if *clientcertAuthMethod != mysql.MysqlClearPassword && *clientcertAuthMethod != mysql.MysqlDialog { + log.Exitf("Invalid mysql_clientcert_auth_method value: only support mysql_clear_password or dialog") + } + ascc := &AuthServerClientCert{ + Method: *clientcertAuthMethod, + } + mysql.RegisterAuthServerImpl("clientcert", ascc) +} + +// AuthMethod is part of the AuthServer interface. +func (ascc *AuthServerClientCert) AuthMethod(user string) (string, error) { + return ascc.Method, nil +} + +// Salt is not used for this plugin. +func (ascc *AuthServerClientCert) Salt() ([]byte, error) { + return mysql.NewSalt() +} + +// ValidateHash is unimplemented. +func (ascc *AuthServerClientCert) ValidateHash(salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, error) { + panic("unimplemented") +} + +// Negotiate is part of the AuthServer interface. +func (ascc *AuthServerClientCert) Negotiate(c *mysql.Conn, user string, remoteAddr net.Addr) (mysql.Getter, error) { + // This code depends on the fact that golang's tls server enforces client cert verification. + // Note that the -mysql_server_ssl_ca flag must be set in order for the vtgate to accept client certs. + // If not set, the vtgate will effectively deny all incoming mysql connections, since they will all lack certificates. + // For more info, check out go/vt/vtttls/vttls.go + certs := c.GetTLSClientCerts() + if certs == nil || len(certs) == 0 { + return nil, fmt.Errorf("no client certs for connection ID %v", c.ConnectionID) + } + + if _, err := mysql.AuthServerReadPacketString(c); err != nil { + return nil, err + } + + return &UserData{ + VTGateCallerID: querypb.VTGateCallerID{ + Username: certs[0].Subject.CommonName, + Groups: certs[0].DNSNames, + }, + }, nil +} + +func (ud *UserData) Get() *querypb.VTGateCallerID { + return &ud.VTGateCallerID +} diff --git a/go/mysql/conn.go b/go/mysql/conn.go index 2cf2ab9978..8fd128b412 100644 --- a/go/mysql/conn.go +++ b/go/mysql/conn.go @@ -18,6 +18,8 @@ package mysql import ( "bufio" + "crypto/tls" + "crypto/x509" "errors" "fmt" "io" @@ -982,3 +984,10 @@ func ParseErrorPacket(data []byte) error { return NewSQLError(int(code), string(sqlState), "%v", msg) } + +func (conn *Conn) GetTLSClientCerts() []*x509.Certificate { + if tlsConn, ok := conn.conn.(*tls.Conn); ok { + return tlsConn.ConnectionState().PeerCertificates + } + return nil +}