Skip to content

Commit

Permalink
Set some defaults from identity file.
Browse files Browse the repository at this point in the history
This commit reduces the amount of typing some users have
to do when using identity file:

* Teleport user is set from the certificate
* Auth preference is set to local

In addition it fixes several UX problems:

* Commands `tctl auth sign` and `tsh login -o`  now include
trusted CA keys in the identity file.

* Command `tsh ssh -i` is now noninteractive and only
uses identity file for authentication, in case
if identity file can not authenticate, no other interactive
or non-interactive authentication methods will be used.
Before this commit, tsh tried to fallback to interactive
login mode or read the keys from agent socket.
  • Loading branch information
klizhentas committed Sep 13, 2018
1 parent a3dfe94 commit a1116b7
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 58 deletions.
4 changes: 2 additions & 2 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,8 +1002,8 @@ func (s *IntSuite) TestTwoClusters(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(outputA.String(), check.Equals, "hello world\n")

// Update known CAs.
err = tc.UpdateKnownCA(context.TODO())
// Update trusted CAs.
err = tc.UpdateTrustedCA(context.TODO())
c.Assert(err, check.IsNil)

// The known_hosts file should have two certificates, the way bytes.Split
Expand Down
26 changes: 15 additions & 11 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,6 @@ func NewClient(c *Config) (tc *TeleportClient, err error) {
if len(c.AuthMethods) == 0 {
return nil, trace.BadParameter("SkipLocalAuth is true but no AuthMethods provided")
}
if c.TLS == nil {
return nil, trace.BadParameter("SkipLocalAuth is true but no TLS config is provided")
}
// if the client was passed an agent in the configuration and skip local auth, use
// the passed in agent.
if c.Agent != nil {
Expand Down Expand Up @@ -1550,27 +1547,34 @@ func (tc *TeleportClient) Login(ctx context.Context, activateKey bool) (*Key, er
return key, nil
}

// UpdateKnownCA connects to the Auth Server and fetches all host certificates
// and updates ~/.tsh/keys/proxy/certs.pem and ~/.tsh/known_hosts.
func (tc *TeleportClient) UpdateKnownCA(ctx context.Context) error {
// GetTrustedCA returns a list of host certificate authorities
// trusted by the cluster client is authenticated with.
func (tc *TeleportClient) GetTrustedCA(ctx context.Context) ([]services.CertAuthority, error) {
// Connect to the proxy.
if !tc.Config.ProxySpecified() {
return trace.BadParameter("proxy server is not specified")
return nil, trace.BadParameter("proxy server is not specified")
}
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}
defer proxyClient.Close()

// Get a client to the Auth Server.
clt, err := proxyClient.ClusterAccessPoint(ctx, true)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}

// Get the list of host certificates that this cluster knows about.
hostCerts, err := clt.GetCertAuthorities(services.HostCA, false)
return clt.GetCertAuthorities(services.HostCA, false)
}

// UpdateTrustedCA connects to the Auth Server and fetches all host certificates
// and updates ~/.tsh/keys/proxy/certs.pem and ~/.tsh/known_hosts.
func (tc *TeleportClient) UpdateTrustedCA(ctx context.Context) error {
// Get the list of host certificates that this cluster knows about.
hostCerts, err := tc.GetTrustedCA(ctx)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -1842,7 +1846,7 @@ func connectToSSHAgent() agent.Agent {
return nil
}

log.Infof("[KEY AGENT] Conneced to System Agent: %q", socketPath)
log.Infof("[KEY AGENT] Connected to the system agent: %q", socketPath)
return agent.NewClient(conn)
}

Expand Down
4 changes: 2 additions & 2 deletions lib/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (s *ClientTestSuite) TestIdentityFileMaking(c *check.C) {
key.Pub = []byte("pub")

// test OpenSSH-compatible identity file creation:
err := MakeIdentityFile(keyFilePath, &key, IdentityFormatOpenSSH)
err := MakeIdentityFile(keyFilePath, &key, IdentityFormatOpenSSH, nil)
c.Assert(err, check.IsNil)

// key is OK:
Expand All @@ -102,7 +102,7 @@ func (s *ClientTestSuite) TestIdentityFileMaking(c *check.C) {

// test standard Teleport identity file creation:
keyFilePath = c.MkDir() + "file"
err = MakeIdentityFile(keyFilePath, &key, IdentityFormatFile)
err = MakeIdentityFile(keyFilePath, &key, IdentityFormatFile, nil)
c.Assert(err, check.IsNil)

// key+cert are OK:
Expand Down
21 changes: 20 additions & 1 deletion lib/client/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"os"

"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshutils"

"github.com/gravitational/trace"
)

Expand Down Expand Up @@ -56,7 +59,7 @@ const (

// MakeIdentityFile takes a username + their credentials and saves them to disk
// in a specified format
func MakeIdentityFile(filePath string, key *Key, format IdentityFileFormat) (err error) {
func MakeIdentityFile(filePath string, key *Key, format IdentityFileFormat, certAuthorities []services.CertAuthority) (err error) {
const (
// the files and the dir will be created with these permissions:
fileMode = 0600
Expand Down Expand Up @@ -86,6 +89,22 @@ func MakeIdentityFile(filePath string, key *Key, format IdentityFileFormat) (err
if _, err = output.Write(key.Cert); err != nil {
return trace.Wrap(err)
}
// append trusted host certificate authorities
for _, ca := range certAuthorities {
for _, publicKey := range ca.GetCheckingKeys() {
data, err := sshutils.MarshalAuthorizedHostsFormat(ca.GetClusterName(), publicKey, nil)
if err != nil {
return trace.Wrap(err)
}
if _, err = output.Write([]byte(data)); err != nil {
return trace.Wrap(err)
}
if _, err = output.Write([]byte("\n")); err != nil {
return trace.Wrap(err)
}
}
}

// dump user identity into separate files:
case IdentityFormatOpenSSH:
keyPath := filePath
Expand Down
44 changes: 44 additions & 0 deletions lib/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ package client

import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"runtime"
"time"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/trace"

Expand All @@ -45,6 +49,46 @@ type Key struct {
// ProxyHost (optionally) contains the hostname of the proxy server
// which issued this key
ProxyHost string

// TrustedCA is a list of trusted certificate authorities
TrustedCA []auth.TrustedCerts
}

// TLSConfig returns client TLS configuration used
// to authenticate against API servers
func (k *Key) ClientTLSConfig() (*tls.Config, error) {
// Because Teleport clients can't be configured (yet), they take the default
// list of cipher suites from Go.
tlsConfig := utils.TLSConfig(nil)

pool := x509.NewCertPool()
for _, ca := range k.TrustedCA {
for _, certPEM := range ca.TLSCertificates {
if !pool.AppendCertsFromPEM(certPEM) {
return nil, trace.BadParameter("failed to parse certificate received from the proxy")
}
}
}
tlsConfig.RootCAs = pool
tlsCert, err := tls.X509KeyPair(k.TLSCert, k.Priv)
if err != nil {
return nil, trace.Wrap(err, "failed to parse TLS cert and key")
}
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
return tlsConfig, nil
}

// CertUsername returns the name of the teleport user encoded in the certificate
func (k *Key) CertUsername() (string, error) {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(k.Cert)
if err != nil {
return "", trace.Wrap(err)
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return "", trace.BadParameter("expected SSH certificate, got public key")
}
return cert.KeyId, nil
}

// AsAgentKeys converts client.Key struct to a []*agent.AddedKey. All elements
Expand Down
3 changes: 2 additions & 1 deletion lib/sshutils/close.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2015 Gravitational, Inc.
Copyright 2015-2018 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sshutils

import (
Expand Down
59 changes: 59 additions & 0 deletions lib/sshutils/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2018 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sshutils

import (
"fmt"
"net/url"
"strings"
)

// MarshalAuthorizedKeysFormat returns the certificate authority public key exported as a single
// line that can be placed in ~/.ssh/authorized_keys file. The format adheres to the
// man sshd (8) authorized_keys format, a space-separated list of: options, keytype,
// base64-encoded key, comment.
// For example:
//
// cert-authority AAA... type=user&clustername=cluster-a
//
// URL encoding is used to pass the CA type and cluster name into the comment field.
func MarshalAuthorizedKeysFormat(clusterName string, keyBytes []byte) (string, error) {
comment := url.Values{
"type": []string{"user"},
"clustername": []string{clusterName},
}

return fmt.Sprintf("cert-authority %s %s", strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
}

// MarshalAuthorizedHostsFormat returns the certificate authority public key exported as a single line
// that can be placed in ~/.ssh/authorized_hosts. The format adheres to the man sshd (8)
// authorized_hosts format, a space-separated list of: marker, hosts, key, and comment.
// For example:
//
// @cert-authority *.cluster-a ssh-rsa AAA... type=host
//
// URL encoding is used to pass the CA type and allowed logins into the comment field.
func MarshalAuthorizedHostsFormat(clusterName string, keyBytes []byte, logins []string) (string, error) {
comment := url.Values{
"type": []string{"host"},
"logins": logins,
}

return fmt.Sprintf("@cert-authority *.%s %s %s",
clusterName, strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
}
31 changes: 12 additions & 19 deletions tool/tctl/common/auth_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package common
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"strings"
"time"
Expand Down Expand Up @@ -310,7 +309,7 @@ func (a *AuthCommand) generateHostKeys(clusterApi auth.ClientI) error {
filePath = principals[0]
}

err = client.MakeIdentityFile(filePath, key, a.outputFormat)
err = client.MakeIdentityFile(filePath, key, a.outputFormat, nil)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -339,8 +338,16 @@ func (a *AuthCommand) generateUserKeys(clusterApi auth.ClientI) error {
return trace.Wrap(err)
}

var certAuthorities []services.CertAuthority
if a.outputFormat == client.IdentityFormatFile {
certAuthorities, err = clusterApi.GetCertAuthorities(services.HostCA, false)
if err != nil {
return trace.Wrap(err)
}
}

// write the cert+private key to the output:
err = client.MakeIdentityFile(a.output, key, a.outputFormat)
err = client.MakeIdentityFile(a.output, key, a.outputFormat, certAuthorities)
if err != nil {
return trace.Wrap(err)
}
Expand All @@ -360,12 +367,7 @@ func (a *AuthCommand) generateUserKeys(clusterApi auth.ClientI) error {
//
// URL encoding is used to pass the CA type and cluster name into the comment field.
func userCAFormat(ca services.CertAuthority, keyBytes []byte) (string, error) {
comment := url.Values{
"type": []string{string(services.UserCA)},
"clustername": []string{ca.GetClusterName()},
}

return fmt.Sprintf("cert-authority %s %s", strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
return sshutils.MarshalAuthorizedKeysFormat(ca.GetClusterName(), keyBytes)
}

// hostCAFormat returns the certificate authority public key exported as a single line
Expand All @@ -377,19 +379,10 @@ func userCAFormat(ca services.CertAuthority, keyBytes []byte) (string, error) {
//
// URL encoding is used to pass the CA type and allowed logins into the comment field.
func hostCAFormat(ca services.CertAuthority, keyBytes []byte, client auth.ClientI) (string, error) {
comment := url.Values{
"type": []string{string(ca.GetType())},
}

roles, err := services.FetchRoles(ca.GetRoles(), client, nil)
if err != nil {
return "", trace.Wrap(err)
}
allowedLogins, _ := roles.CheckLoginDuration(defaults.MinCertDuration + time.Second)
if len(allowedLogins) > 0 {
comment["logins"] = allowedLogins
}

return fmt.Sprintf("@cert-authority *.%s %s %s",
ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
return sshutils.MarshalAuthorizedHostsFormat(ca.GetClusterName(), keyBytes, allowedLogins)
}
Loading

0 comments on commit a1116b7

Please sign in to comment.