mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 01:34:01 +00:00
got u2f login working on the CLI client.
also grouped the u2f webapi endpoints together, and fixed the default u2f AppID so it works out of the box
This commit is contained in:
parent
b24fc74e48
commit
739308c5ae
|
@ -119,6 +119,8 @@ func NewStandardPermissions() PermissionChecker {
|
|||
|
||||
sp.permissions[teleport.RoleU2fUser] = map[string]bool{
|
||||
ActionPreAuthSignIn: true,
|
||||
ActionGenerateUserCert: true,
|
||||
ActionGetCertAuthorities: true,
|
||||
}
|
||||
|
||||
return &sp
|
||||
|
@ -165,6 +167,7 @@ var StandardRoles = teleport.Roles{
|
|||
teleport.RoleProvisionToken,
|
||||
teleport.RoleSignup,
|
||||
teleport.RoleU2fSign,
|
||||
teleport.RoleU2fUser,
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -119,6 +119,10 @@ type Config struct {
|
|||
// that uses local cache to validate hosts
|
||||
HostKeyCallback HostKeyCallback
|
||||
|
||||
// SecondFactorType indicates whether HOTP, OIDC or U2F should be used
|
||||
// for the second factor
|
||||
SecondFactorType string
|
||||
|
||||
// ConnectorID is used to authenticate user via OpenID Connect
|
||||
// registered connector
|
||||
ConnectorID string
|
||||
|
@ -810,18 +814,24 @@ func (tc *TeleportClient) Login() error {
|
|||
}
|
||||
|
||||
var response *web.SSHLoginResponse
|
||||
if tc.ConnectorID == "" {
|
||||
switch tc.SecondFactorType {
|
||||
case "hotp":
|
||||
response, err = tc.directLogin(key.Pub)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
case "oidc":
|
||||
response, err = tc.oidcLogin(tc.ConnectorID, key.Pub)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
// in this case identity is returned by the proxy
|
||||
tc.Username = response.Username
|
||||
case "u2f":
|
||||
response, err = tc.u2fLogin(key.Pub)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
key.Cert = response.Cert
|
||||
// save the key:
|
||||
|
@ -907,6 +917,30 @@ func (tc *TeleportClient) oidcLogin(connectorID string, pub []byte) (*web.SSHLog
|
|||
return response, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// directLogin asks for a password and performs the challenge-response authentication
|
||||
func (tc *TeleportClient) u2fLogin(pub []byte) (*web.SSHLoginResponse, error) {
|
||||
// U2F login requires the official u2f-host executable
|
||||
_, err := exec.LookPath("u2f-host")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpsProxyHostPort := tc.Config.ProxyHostPort(true)
|
||||
certPool := loopbackPool(httpsProxyHostPort)
|
||||
|
||||
password, err := tc.AskPassword()
|
||||
|
||||
response, err := web.SSHAgentU2fLogin(httpsProxyHostPort,
|
||||
tc.Config.Username,
|
||||
password,
|
||||
pub,
|
||||
tc.KeyTTL,
|
||||
tc.InsecureSkipVerify,
|
||||
certPool)
|
||||
|
||||
return response, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// loopbackPool reads trusted CAs if it finds it in a predefined location
|
||||
// and will work only if target proxy address is loopback
|
||||
func loopbackPool(proxyAddr string) *x509.CertPool {
|
||||
|
@ -983,6 +1017,18 @@ func (tc *TeleportClient) AskPasswordAndHOTP() (pwd string, token string, err er
|
|||
return pwd, token, nil
|
||||
}
|
||||
|
||||
// AskPassword prompts the user to enter the password
|
||||
func (tc *TeleportClient) AskPassword() (pwd string, err error) {
|
||||
fmt.Printf("Enter password for Teleport user %v:\n", tc.Config.Username)
|
||||
pwd, err = passwordFromConsole()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
// passwordFromConsole reads from stdin without echoing typed characters to stdout
|
||||
func passwordFromConsole() (string, error) {
|
||||
fd := syscall.Stdin
|
||||
|
|
|
@ -233,8 +233,8 @@ func MakeSampleFileConfig() (fc *FileConfig) {
|
|||
var a Auth
|
||||
a.ListenAddress = conf.Auth.SSHAddr.Addr
|
||||
a.EnabledFlag = "yes"
|
||||
a.U2fAppId = conf.Hostname
|
||||
a.U2fTrustedFacets = []string{conf.Hostname}
|
||||
a.U2fAppId = conf.Auth.U2fAppId
|
||||
a.U2fTrustedFacets = conf.Auth.U2fTrustedFacets
|
||||
|
||||
// sample proxy config:
|
||||
var p Proxy
|
||||
|
|
|
@ -306,8 +306,8 @@ func ApplyDefaults(cfg *Config) {
|
|||
cfg.Auth.KeysBackend.Params = boltParams(defaults.DataDir, defaults.KeysBoltFile)
|
||||
cfg.Auth.RecordsBackend.Type = defaults.BackendType
|
||||
cfg.Auth.RecordsBackend.Params = boltParams(defaults.DataDir, defaults.RecordsBoltFile)
|
||||
cfg.Auth.U2fAppId = hostname
|
||||
cfg.Auth.U2fTrustedFacets = []string{hostname}
|
||||
cfg.Auth.U2fAppId = "https://" + hostname
|
||||
cfg.Auth.U2fTrustedFacets = []string{fmt.Sprintf("%s:%d", cfg.Auth.U2fAppId, defaults.HTTPListenPort)}
|
||||
defaults.ConfigureLimiter(&cfg.Auth.Limiter)
|
||||
|
||||
// defaults for the SSH proxy service:
|
||||
|
|
|
@ -260,9 +260,6 @@ func (s *sessionCache) AuthWithU2fSignResponse(user string, u2fSignResponse *u2f
|
|||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
// we are always closing this client, because we will not be using
|
||||
// this connection initiated using password based credentials
|
||||
// down the road, so it's a one call client
|
||||
defer clt.Close()
|
||||
session, err := clt.PreAuthenticatedSignIn(user)
|
||||
if err != nil {
|
||||
|
@ -303,6 +300,35 @@ func (s *sessionCache) GetCertificate(c createSSHCertReq) (*SSHLoginResponse, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *sessionCache) GetCertificateWithU2f(c createSSHCertWithU2fReq) (*SSHLoginResponse, error) {
|
||||
method, err := auth.NewWebU2fSignResponseAuth(c.User, &c.U2fSignResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
clt, err := auth.NewTunClient("web.auth-u2f-sign-response", s.authServers, c.User, method)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
cert, err := clt.GenerateUserCert(c.PubKey, c.User, c.TTL)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
hostSigners, err := clt.GetCertAuthorities(services.HostCA, false)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
signers := []services.CertAuthority{}
|
||||
for _, hs := range hostSigners {
|
||||
signers = append(signers, *hs)
|
||||
}
|
||||
|
||||
return &SSHLoginResponse{
|
||||
Cert: cert,
|
||||
HostSigners: signers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sessionCache) GetUserInviteInfo(token string) (user string,
|
||||
QRImg []byte, hotpFirstValues []string, e error) {
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ package web
|
|||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -16,6 +18,8 @@ import (
|
|||
"github.com/gravitational/roundtrip"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/mailgun/lemma/secret"
|
||||
|
||||
"github.com/tstranex/u2f"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -196,6 +200,89 @@ func SSHAgentLogin(proxyAddr, user, password, hotpToken string, pubKey []byte, t
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// SSHAgentU2fLogin requests a U2F sign request (authentication challenge) via the proxy.
|
||||
// If the credentials are valid, the proxy wiil return a challenge.
|
||||
// We then call the official u2f-host binary to perform the signing and pass the signature to the proxy.
|
||||
// If the authentication succeeds, we will get a temporary certificate back
|
||||
func SSHAgentU2fLogin(proxyAddr, user, password string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool) (*SSHLoginResponse, error) {
|
||||
clt, _, err := initClient(proxyAddr, insecure, pool)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
u2fSignRequest, err := clt.PostJSON(clt.Endpoint("webapi", "u2f", "sign_request"), u2fSignRequestReq{
|
||||
User: user,
|
||||
Pass: password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Pass the JSON-encoded data undecoded to the u2f-host binary
|
||||
u2fFacet := "https://" + proxyAddr
|
||||
cmd := exec.Command("u2f-host", "-aauthenticate", "-o", u2fFacet)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cmd.Start()
|
||||
stdin.Write(u2fSignRequest.Bytes())
|
||||
stdin.Close()
|
||||
fmt.Println("Please press the button on your U2F key")
|
||||
|
||||
// The origin URL is passed back base64-encoded and the keyHandle is passed back as is.
|
||||
// A very long proxy hostname or keyHandle can overflow a fixed-size buffer.
|
||||
signResponseLen := 500 + len(u2fSignRequest.Bytes()) + len(proxyAddr) * 4 / 3
|
||||
signResponseBuf := make([]byte, signResponseLen)
|
||||
signResponseLen, err = io.ReadFull(stdout, signResponseBuf)
|
||||
// unexpected EOF means we have read the data completely.
|
||||
if err == nil {
|
||||
return nil, errors.New("u2f sign response exceeded buffer size")
|
||||
}
|
||||
|
||||
// Read error message (if any). 100 bytes is more than enough for any error message u2f-host outputs
|
||||
errMsgBuf := make([]byte, 100)
|
||||
errMsgLen, _ := io.ReadFull(stderr, errMsgBuf)
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return nil, errors.New("u2f-host returned error: " + string(errMsgBuf[:errMsgLen]))
|
||||
}
|
||||
|
||||
var u2fSignResponse *u2f.SignResponse
|
||||
err = json.Unmarshal(signResponseBuf[:signResponseLen], &u2fSignResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
re, err := clt.PostJSON(clt.Endpoint("webapi", "u2f", "certs"), createSSHCertWithU2fReq{
|
||||
User: user,
|
||||
U2fSignResponse: *u2fSignResponse,
|
||||
PubKey: pubKey,
|
||||
TTL: ttl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var out *SSHLoginResponse
|
||||
err = json.Unmarshal(re.Bytes(), &out)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func initClient(proxyAddr string, insecure bool, pool *x509.CertPool) (*webClient, *url.URL, error) {
|
||||
// validate proxyAddr:
|
||||
host, port, err := net.SplitHostPort(proxyAddr)
|
||||
|
|
|
@ -125,16 +125,12 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*Handler, error) {
|
|||
|
||||
// Web sessions
|
||||
h.POST("/webapi/sessions", httplib.MakeHandler(h.createSession))
|
||||
h.POST("/webapi/sessions_u2f_sign", httplib.MakeHandler(h.u2fSignRequest))
|
||||
h.POST("/webapi/sessions_u2f", httplib.MakeHandler(h.createSessionWithU2fSignResponse))
|
||||
h.DELETE("/webapi/sessions/:sid", h.withAuth(h.deleteSession))
|
||||
h.POST("/webapi/sessions/renew", h.withAuth(h.renewSession))
|
||||
|
||||
// Users
|
||||
h.GET("/webapi/users/invites/:token", httplib.MakeHandler(h.renderUserInvite))
|
||||
h.GET("/webapi/users/invites_u2f_register/:token", httplib.MakeHandler(h.u2fRegisterRequest))
|
||||
h.POST("/webapi/users", httplib.MakeHandler(h.createNewUser))
|
||||
h.POST("/webapi/users_u2f", httplib.MakeHandler(h.createNewU2fUser))
|
||||
|
||||
// Issues SSH temp certificates based on 2FA access creds
|
||||
h.POST("/webapi/ssh/certs", httplib.MakeHandler(h.createSSHCert))
|
||||
|
@ -170,6 +166,13 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*Handler, error) {
|
|||
h.POST("/webapi/oidc/login/console", httplib.MakeHandler(h.oidcLoginConsole))
|
||||
h.GET("/webapi/oidc/callback", httplib.MakeHandler(h.oidcCallback))
|
||||
|
||||
// U2F related APIs
|
||||
h.GET("/webapi/u2f/invite_register_request/:token", httplib.MakeHandler(h.u2fRegisterRequest))
|
||||
h.POST("/webapi/u2f/new_user", httplib.MakeHandler(h.createNewU2fUser))
|
||||
h.POST("/webapi/u2f/sign_request", httplib.MakeHandler(h.u2fSignRequest))
|
||||
h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.createSessionWithU2fSignResponse))
|
||||
h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.createSSHCertWithU2fSignResponse))
|
||||
|
||||
// if Web UI is enabled, chekc the assets dir:
|
||||
var (
|
||||
writeSettings http.HandlerFunc
|
||||
|
@ -1232,6 +1235,35 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro
|
|||
return cert, nil
|
||||
}
|
||||
|
||||
// createSSHCertWithU2fReq are passed by web client
|
||||
// to authenticate against teleport server and receive
|
||||
// a temporary cert signed by auth server authority
|
||||
type createSSHCertWithU2fReq struct {
|
||||
// User is a teleport username
|
||||
User string `json:"user"`
|
||||
// We only issue U2F sign requests after checking the password, so there's no need to check again.
|
||||
// U2fSignResponse is the signature from the U2F device
|
||||
U2fSignResponse u2f.SignResponse `json:"u2f_sign_response"`
|
||||
// PubKey is a public key user wishes to sign
|
||||
PubKey []byte `json:"pub_key"`
|
||||
// TTL is a desired TTL for the cert (max is still capped by server,
|
||||
// however user can shorten the time)
|
||||
TTL time.Duration `json:"ttl"`
|
||||
}
|
||||
|
||||
func (h *Handler) createSSHCertWithU2fSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
var req *createSSHCertWithU2fReq
|
||||
if err := httplib.ReadJSON(r, &req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cert, err := h.auth.GetCertificateWithU2f(*req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (h *Handler) String() string {
|
||||
return fmt.Sprintf("multi site")
|
||||
}
|
||||
|
|
2
roles.go
2
roles.go
|
@ -107,7 +107,7 @@ const (
|
|||
RoleProvisionToken Role = "ProvisionToken"
|
||||
// RoleSignup is for first time signing up users
|
||||
RoleSignup Role = "Signup"
|
||||
// RoleU2fSign is for requesting a U2F auth challenge
|
||||
// RoleU2fSign is for partially authenticated U2F users who need to request a U2F auth challenge
|
||||
RoleU2fSign = "U2fSign"
|
||||
// RoleU2fUser is for teleport SSH user already authenticated with U2F
|
||||
RoleU2fUser = "U2fUser"
|
||||
|
|
|
@ -73,6 +73,8 @@ type CLIConf struct {
|
|||
LocalExec bool
|
||||
// ExternalAuth is used to authenticate using external OIDC method
|
||||
ExternalAuth string
|
||||
// U2f switches to logging in using U2F as the second factor
|
||||
U2f bool
|
||||
// SiteName specifies remote site go login to
|
||||
SiteName string
|
||||
// Interactive, when set to true, launches remote command with the terminal attached
|
||||
|
@ -92,6 +94,7 @@ func run(args []string, underTest bool) {
|
|||
app.Flag("login", "Remote host login").Short('l').Envar("TELEPORT_LOGIN").StringVar(&cf.NodeLogin)
|
||||
app.Flag("user", fmt.Sprintf("SSH proxy user [%s]", client.Username())).Envar("TELEPORT_USER").StringVar(&cf.Username)
|
||||
app.Flag("auth", "[EXPERIMENTAL] Use external authentication, e.g. 'google'").Envar("TELEPORT_AUTH").Hidden().StringVar(&cf.ExternalAuth)
|
||||
app.Flag("u2f", "Use U2F as second factor").Default("false").BoolVar(&cf.U2f)
|
||||
app.Flag("cluster", "Specify the cluster to connect").Envar("TELEPORT_SITE").StringVar(&cf.SiteName)
|
||||
app.Flag("proxy", "SSH proxy host or IP address").Envar("TELEPORT_PROXY").StringVar(&cf.Proxy)
|
||||
app.Flag("ttl", "Minutes to live for a SSH session").Int32Var(&cf.MinsToLive)
|
||||
|
@ -383,6 +386,15 @@ func makeClient(cf *CLIConf) (tc *client.TeleportClient, err error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var secondFactorType string
|
||||
if cf.ExternalAuth != "" {
|
||||
secondFactorType = "oidc"
|
||||
} else if cf.U2f {
|
||||
secondFactorType = "u2f"
|
||||
} else {
|
||||
secondFactorType = "hotp"
|
||||
}
|
||||
|
||||
// prep client config:
|
||||
c := &client.Config{
|
||||
Stdout: os.Stdout,
|
||||
|
@ -397,6 +409,7 @@ func makeClient(cf *CLIConf) (tc *client.TeleportClient, err error) {
|
|||
KeyTTL: time.Minute * time.Duration(cf.MinsToLive),
|
||||
InsecureSkipVerify: cf.InsecureSkipVerify,
|
||||
LocalForwardPorts: fPorts,
|
||||
SecondFactorType: secondFactorType,
|
||||
ConnectorID: cf.ExternalAuth,
|
||||
SiteName: cf.SiteName,
|
||||
Interactive: cf.Interactive,
|
||||
|
|
Loading…
Reference in a new issue