diff --git a/constants.go b/constants.go index 54db96d4195c0..d9bbead9e6886 100644 --- a/constants.go +++ b/constants.go @@ -102,6 +102,13 @@ const ( OIDC = "oidc" ) +const ( + // AuthorizedKeys are public keys that check against User CAs. + AuthorizedKeys = "authorized_keys" + // KnownHosts are public keys that check against Host CAs. + KnownHosts = "known_hosts" +) + const ( // CertExtensionPermitAgentForwarding allows agent forwarding for certificate CertExtensionPermitAgentForwarding = "permit-agent-forwarding" diff --git a/lib/auth/init.go b/lib/auth/init.go index 73f014061609f..b82d17f81763f 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -188,7 +188,7 @@ func Init(cfg InitConfig, dynamicConfig bool) (*AuthServer, *Identity, error) { if err := asrv.Trust.UpsertCertAuthority(ca, backend.Forever); err != nil { return nil, nil, trace.Wrap(err) } - log.Infof("[INIT] Created Trusted Certificate Authority: %v", ca) + log.Infof("[INIT] Created Trusted Certificate Authority: %q, type: %q", ca.GetName(), ca.GetType()) } for _, tunnel := range cfg.ReverseTunnels { diff --git a/lib/config/configuration.go b/lib/config/configuration.go index a0c0ca4349ad8..9a49081adf37b 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -413,9 +413,42 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error { return nil } -// parseCAKey() gets called for every line in a "CA key file" which is -// the same as 'known_hosts' format for openssh -func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) { +// parseAuthorizedKeys parses keys in the authorized_keys format and +// returns a services.CertAuthority. +func parseAuthorizedKeys(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) { + pubkey, comment, _, _, err := ssh.ParseAuthorizedKey(bytes) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + comments, err := url.ParseQuery(comment) + if err != nil { + return nil, nil, trace.Wrap(err) + } + clusterName := comments.Get("clustername") + if clusterName == "" { + return nil, nil, trace.BadParameter("not clustername provided") + } + + // create a new certificate authority + ca := services.NewCertAuthority( + services.UserCA, + clusterName, + nil, + [][]byte{ssh.MarshalAuthorizedKey(pubkey)}, + allowedLogins) + + // transform old allowed logins into roles + role := services.RoleForCertAuthority(ca) + role.SetLogins(allowedLogins) + ca.AddRole(role.GetName()) + + return ca, role, nil +} + +// parseKnownHosts parses keys in known_hosts format and returns a +// services.CertAuthority. +func parseKnownHosts(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) { marker, options, pubKey, comment, _, err := ssh.ParseKnownHosts(bytes) if marker != "cert-authority" { return nil, nil, trace.BadParameter("invalid file format. expected '@cert-authority` marker") @@ -447,6 +480,34 @@ func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, s return ca, role, nil } +// certificateAuthorityFormat parses bytes and determines if they are in +// known_hosts format or authorized_keys format. +func certificateAuthorityFormat(bytes []byte) (string, error) { + _, _, _, _, err := ssh.ParseAuthorizedKey(bytes) + if err != nil { + _, _, _, _, _, err := ssh.ParseKnownHosts(bytes) + if err != nil { + return "", trace.BadParameter("unknown ca format") + } + return teleport.KnownHosts, nil + } + return teleport.AuthorizedKeys, nil +} + +// parseCAKey parses bytes either in known_hosts or authorized_keys format +// and returns a services.CertAuthority. +func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) { + caFormat, err := certificateAuthorityFormat(bytes) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + if caFormat == teleport.AuthorizedKeys { + return parseAuthorizedKeys(bytes, allowedLogins) + } + return parseKnownHosts(bytes, allowedLogins) +} + // readTrustedClusters parses the content of "trusted_clusters" YAML structure // and modifies Teleport 'conf' by adding "authorities" and "reverse tunnels" // to it @@ -489,7 +550,9 @@ func readTrustedClusters(clusters []TrustedCluster, conf *service.Config) error ca.GetClusterName()) } authorities = append(authorities, ca) - roles = append(roles, role) + if role != nil { + roles = append(roles, role) + } } conf.Auth.Authorities = append(conf.Auth.Authorities, authorities...) conf.Auth.Roles = append(conf.Auth.Roles, roles...) diff --git a/lib/config/configuration_test.go b/lib/config/configuration_test.go index 277554f548e8f..d718bc6b8f839 100644 --- a/lib/config/configuration_test.go +++ b/lib/config/configuration_test.go @@ -19,6 +19,7 @@ package config import ( "bytes" "encoding/base64" + "fmt" "io/ioutil" "net" "os" @@ -48,6 +49,7 @@ type ConfigTestSuite struct { } var _ = check.Suite(&ConfigTestSuite{}) +var _ = fmt.Printf func (s *ConfigTestSuite) SetUpSuite(c *check.C) { var err error @@ -376,6 +378,45 @@ func (s *ConfigTestSuite) TestLegacyU2FTransformation(c *check.C) { c.Assert(cfg.Auth.U2F.GetFacets(), check.DeepEquals, []string{"https://graviton:3080"}) } +// TestParseKey ensures that keys are parsed correctly if they are in +// authorized_keys format or known_hosts format. +func (s *ConfigTestSuite) TestParseKey(c *check.C) { + tests := []struct { + inCABytes []byte + outType services.CertAuthType + outClusterName string + }{ + // 0 - host ca in known_hosts format + { + []byte(`@cert-authority *.foo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCz+PzY6z2Xa1cMeiJqOH5BRpwY+PlS3Q6C4e3Yj8xjLW1zD3Cehm71zjsYmrpuFTmdylbcKB6CcM6Ft4YbKLG3PTLSKvCPTgfSBk8RCYX02PtOV5ixwa7xl5Gfhc1GRIheXgFO9IT+W9w9ube9r002AGpkMnRRtWAWiZHMGeJoaUoCsjDLDbWsQHj06pr7fD98c7PVcVzCKPTQpadXEP6sF8w417DvypHY1bYsvhRqHw9Njx6T3b9BM3bJ4QXgy18XuO5fCpLjKLsngLwSbqe/1IP4Q0zlUaNOTph3WnjeKJZO9yQeVX1cWDwY4Iz5lSHhsJnQD99hBDdw2RklHU0j type=host`), + services.HostCA, + "foo", + }, + // 1 - user ca in known_hosts format (legacy) + { + []byte(`@cert-authority *.bar ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfhrvzbHAHrukeDhLSzoXtpctiumao1MQElwhOeuzFRYwrGV/1L2gsx4OJk4ztXKOCpon1FB+dy2aJN0WIr/9qXg37D6K/XJhgDaSfW8cjpl72Lw8kknDpmgSSA3cTvzFNmXfw4DNT/klRwEw6MMrDmfT9QvaV2d35lSoMMeTZ1ilFeJqXdUkY+bgijLBQU5MUjZUfQfS3jpSxVD0DD9D1VbAE1nGSNyFqf34JxJmqJ3R5hfZqNfb9CWouv+uFF99tzOr7tnKM/sQMPGmJ5G+zjTaErNSSLiIU1iCwVKUpNFcGiR1lpOEET+neJVnEeqEqKv2ookkXaIdKjk1UKZEn type=user`), + services.UserCA, + "bar", + }, + // 2 - user ca in authorized_keys format + { + []byte(`cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiIxyz0ctsyQbKLpWVYNF+ZIOrF150Wma2GqkrOWZaOzu5NSnt9Hmp7DaIa2Gn8fh8+8vjP02qp3i43SDOlLyYSn05nJjEXaz7QGysgeppN8ayojl5dkOhA00ROpCl5HhS9cmga7fy1Uwy4jhxenNpfQ5ap0COQi3UrXPepaq8z+I4XQK//qFWnkgyD1VXCnRKXXiajOf3dShYJqLCgwYiViuFmzi2p3lysoYS5eRwTCKiyyBtlkUtpTAse455yGf3QCpe+UOBiJ/4AElxacDndtMkjjctHSPCiztnph1xej64vSy8C2nGsnPIK7RfiOzSEdd5hwva+wPLgNTcKXZz type=user&clustername=baz`), + services.UserCA, + "baz", + }, + } + + // run tests + for i, tt := range tests { + comment := check.Commentf("Test %v", i) + + ca, _, err := parseCAKey(tt.inCABytes, []string{"foo"}) + c.Assert(err, check.IsNil, comment) + c.Assert(ca.GetType(), check.Equals, tt.outType) + c.Assert(ca.GetClusterName(), check.Equals, tt.outClusterName) + } +} + func checkStaticConfig(c *check.C, conf *FileConfig) { c.Assert(conf.AuthToken, check.Equals, "xxxyyy") c.Assert(conf.SSH.Enabled(), check.Equals, false) // YAML treats 'no' as False diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index 007fb40e8e772..01e2536de177e 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -89,6 +89,7 @@ type AuthCommand struct { exportAuthorityFingerprint string exportPrivateKeys bool outDir string + compatVersion string } type AuthServerCommand struct { @@ -217,6 +218,7 @@ func Run() { authExport := auth.Command("export", "Export CA keys to standard output") authExport.Flag("keys", "if set, will print private keys").BoolVar(&cmdAuth.exportPrivateKeys) authExport.Flag("fingerprint", "filter authority by fingerprint").StringVar(&cmdAuth.exportAuthorityFingerprint) + authExport.Flag("compat", "export cerfiticates compatible with specific version of Teleport").StringVar(&cmdAuth.compatVersion) authGenerate := auth.Command("gen", "Generate a new SSH keypair") authGenerate.Flag("pub-key", "path to the public key").Required().StringVar(&cmdAuth.genPubPath) @@ -613,6 +615,18 @@ func (a *AuthCommand) ExportAuthorities(client *auth.TunClient) error { continue } + // export certificates in the old 1.0 format where host and user + // certificate authorities were exported in the known_hosts format. + if a.compatVersion == "1.0" { + castr, err := hostCAFormat(ca, keyBytes, client) + if err != nil { + return trace.Wrap(err) + } + + fmt.Println(castr) + continue + } + // export certificate authority in user or host ca format var castr string switch ca.GetType() { @@ -636,24 +650,34 @@ func (a *AuthCommand) ExportAuthorities(client *auth.TunClient) error { } // userCAFormat returns the certificate authority public key exported as a single -// line that can be placed in ~/.ssh/authorized_keys file. For example: +// 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 // -// cert-authority AAA... +// 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) { - return fmt.Sprintf("cert-authority %s", keyBytes), nil + comment := url.Values{ + "type": []string{string(services.UserCA)}, + "clustername": []string{ca.GetClusterName()}, + } + + return fmt.Sprintf("cert-authority %s %s", keyBytes, comment.Encode()), nil } // hostCAFormat 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: makrer, hosts, key, and comment. +// authorized_hosts format, a space-separated list of: marker, hosts, key, and comment. // For example: // -// @cert-authority *.cluster-a ssh-rsa AAA... type=host +// @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 hostCAFormat(ca services.CertAuthority, keyBytes []byte, client *auth.TunClient) (string, error) { - options := url.Values{ - "type": []string{string(services.HostCA)}, + comment := url.Values{ + "type": []string{string(ca.GetType())}, } roles, err := services.FetchRoles(ca.GetRoles(), client) @@ -662,11 +686,11 @@ func hostCAFormat(ca services.CertAuthority, keyBytes []byte, client *auth.TunCl } allowedLogins, _ := roles.CheckLogins(defaults.MinCertDuration + time.Second) if len(allowedLogins) > 0 { - options["logins"] = allowedLogins + comment["logins"] = allowedLogins } return fmt.Sprintf("@cert-authority *.%s %s %s", - ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), options.Encode()), nil + ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), comment.Encode()), nil } // GenerateKeys generates a new keypair