Add where predicate and Machine ID support to SSH host certificates (#16261)

* Add `where` predicate and Machine ID support to SSH host certificates

This adds `where` RBAC predicate support to `host_cert`
pseudo-resources, and adds support for requesting host certs to
Machine ID (with or without these RBAC predicates).

More specifically, this:
 * Makes 3 new set functions available during `where` predicate
   evaluation: `all_equal`, `all_end_with`, and `is_subset`. These
   make it possible to examine the `host_cert.principals` field.
 * Adds a new optional `host_cert` field to the standard `where`
   context. It's only ever set specifically in `GenerateHostCert()`.
 * Sets a custom context in `GenerateHostCert()` to pass along host
   cert parameters.
 * Makes current config available to tbot config templates
 * Adds a new `ssh_host_cert` config template to Machine ID
 * Refactors `tbot/config` tests to use shared mock bot / auth
   implementations.

Host certs can be requested in Machine ID by adding a new config
template:
```yaml
destinations:
  - directory: /opt/machine-id
    configs:
      - ssh_host_cert:
          principals: [foo.example.com]
```

Bots will need a role like the following:
```yaml
kind: role
metadata:
  name: hostcert-bot
version: v5
spec:
  allow:
    rules:
      - resources:
          - host_cert
        verbs:
          - create
        where: 'is_subset(host_cert.principals, "foo.example.com")'
```

See the RFD for more information:
https://github.com/gravitational/teleport/blob/master/rfd/0083-machine-id-host-certs.md

* Fix lint failure

* Improve doc comments on the new predicate functions.

* Address review feedback

* Address review feedback

- Remove `AuthenticatedUserClientFromIdentity`'s unnecessary
  AuthServer param
- Add missing doc comments
- Pull out `getTestIdent` from the Kubernetes template test
- Move various test helpers into their own file
- Unexport new predicate functions
This commit is contained in:
Tim Buckley
2022-09-14 20:18:05 +00:00
committed by GitHub
parent fffe28555e
commit 81f42bbaed
16 changed files with 921 additions and 291 deletions
+20 -1
View File
@@ -2110,9 +2110,28 @@ func (a *ServerWithRoles) DeleteUser(ctx context.Context, user string) error {
func (a *ServerWithRoles) GenerateHostCert(
key []byte, hostID, nodeName string, principals []string, clusterName string, role types.SystemRole, ttl time.Duration,
) ([]byte, error) {
if err := a.action(apidefaults.Namespace, types.KindHostCert, types.VerbCreate); err != nil {
ctx := services.Context{
User: a.context.User,
HostCert: &services.HostCertContext{
HostID: hostID,
NodeName: nodeName,
Principals: principals,
ClusterName: clusterName,
Role: role,
TTL: ttl,
},
}
// Instead of the usual RBAC checks, we'll manually call CheckAccessToRule
// here as we'll be evaluating `where` predicates with a custom RuleContext
// to expose cert request fields.
// We've only got a single verb to check so luckily it's pretty concise.
if err := a.withOptions().context.Checker.CheckAccessToRule(
&ctx, apidefaults.Namespace, types.KindHostCert, types.VerbCreate, false,
); err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GenerateHostCert(key, hostID, nodeName, principals, clusterName, role, ttl)
}
+142
View File
@@ -3645,3 +3645,145 @@ func TestListResources_WithRoles(t *testing.T) {
})
}
}
// TestGenerateHostCert attempts to generate host certificates using various
// RBAC rules
func TestGenerateHostCert(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clusterName := srv.ClusterName()
_, pub, err := native.GenerateKeyPair()
require.NoError(t, err)
noError := func(err error) bool {
return err == nil
}
for _, test := range []struct {
desc string
principals []string
skipRule bool
where string
deny bool
denyWhere string
expect func(error) bool
}{
{
desc: "disallowed",
skipRule: true,
principals: []string{"foo.example.com"},
expect: trace.IsAccessDenied,
},
{
desc: "denied",
deny: true,
principals: []string{"foo.example.com"},
expect: trace.IsAccessDenied,
},
{
desc: "allowed",
principals: []string{"foo.example.com"},
expect: noError,
},
{
desc: "allowed-subset",
principals: []string{"foo.example.com"},
where: `is_subset(host_cert.principals, "foo.example.com", "bar.example.com")`,
expect: noError,
},
{
desc: "disallowed-subset",
principals: []string{"baz.example.com"},
where: `is_subset(host_cert.principals, "foo.example.com", "bar.example.com")`,
expect: trace.IsAccessDenied,
},
{
desc: "allowed-all-equal",
principals: []string{"foo.example.com"},
where: `all_equal(host_cert.principals, "foo.example.com")`,
expect: noError,
},
{
desc: "disallowed-all-equal",
principals: []string{"bar.example.com"},
where: `all_equal(host_cert.principals, "foo.example.com")`,
expect: trace.IsAccessDenied,
},
{
desc: "allowed-all-end-with",
principals: []string{"node.foo.example.com"},
where: `all_end_with(host_cert.principals, ".foo.example.com")`,
expect: noError,
},
{
desc: "disallowed-all-end-with",
principals: []string{"node.bar.example.com"},
where: `all_end_with(host_cert.principals, ".foo.example.com")`,
expect: trace.IsAccessDenied,
},
{
desc: "allowed-complex",
principals: []string{"foo.example.com"},
where: `all_end_with(host_cert.principals, ".example.com")`,
denyWhere: `is_subset(host_cert.principals, "bar.example.com", "baz.example.com")`,
expect: noError,
},
{
desc: "disallowed-complex",
principals: []string{"bar.example.com"},
where: `all_end_with(host_cert.principals, ".example.com")`,
denyWhere: `is_subset(host_cert.principals, "bar.example.com", "baz.example.com")`,
expect: trace.IsAccessDenied,
},
{
desc: "allowed-multiple",
principals: []string{"bar.example.com", "foo.example.com"},
where: `is_subset(host_cert.principals, "foo.example.com", "bar.example.com")`,
expect: noError,
},
{
desc: "disallowed-multiple",
principals: []string{"foo.example.com", "bar.example.com", "baz.example.com"},
where: `is_subset(host_cert.principals, "foo.example.com", "bar.example.com")`,
expect: trace.IsAccessDenied,
},
} {
t.Run(test.desc, func(t *testing.T) {
rules := []types.Rule{}
if !test.skipRule {
rules = append(rules, types.Rule{
Resources: []string{types.KindHostCert},
Verbs: []string{types.VerbCreate},
Where: test.where,
})
}
denyRules := []types.Rule{}
if test.deny || test.denyWhere != "" {
denyRules = append(denyRules, types.Rule{
Resources: []string{types.KindHostCert},
Verbs: []string{types.VerbCreate},
Where: test.denyWhere,
})
}
role, err := CreateRole(ctx, srv.Auth(), test.desc, types.RoleSpecV5{
Allow: types.RoleConditions{Rules: rules},
Deny: types.RoleConditions{Rules: denyRules},
})
require.NoError(t, err)
user, err := CreateUser(srv.Auth(), test.desc, role)
require.NoError(t, err)
client, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
_, err = client.GenerateHostCert(pub, "", "", test.principals, clusterName, types.RoleNode, 0)
require.True(t, test.expect(err))
})
}
}
+128 -2
View File
@@ -53,6 +53,93 @@ var (
CertAuthorityTypeExpr = builder.Identifier(`system.catype()`)
)
// predicateAllEndWith is a custom function to test if a string ends with a
// particular suffix. If given a `[]string` as the first argument, all values
// must have the given suffix (2nd argument).
func predicateAllEndWith(a interface{}, b interface{}) predicate.BoolPredicate {
return func() bool {
// bval is the suffix and must always be a plain string.
bval, ok := b.(string)
if !ok {
return false
}
switch aval := a.(type) {
case string:
return strings.HasSuffix(aval, bval)
case []string:
for _, val := range aval {
if !strings.HasSuffix(val, bval) {
return false
}
}
return true
default:
return false
}
}
}
// predicateAllEqual is a custom function to test if all entries in a []string
// are equal to a certain value. This is primarily useful for comparing string
// fields that are only expected to contain a single, specific value.
func predicateAllEqual(a interface{}, b interface{}) predicate.BoolPredicate {
return func() bool {
// bval is the suffix and must always be a plain string.
bval, ok := b.(string)
if !ok {
return false
}
switch aval := a.(type) {
case string:
return aval == bval
case []string:
for _, val := range aval {
if val != bval {
return false
}
}
return true
default:
return false
}
}
}
// predicateIsSubset determines if the first parameter is contained within the
// variadic args. The first argument may either by `string` or `[]string`, and
// the variadic args may only be `string`.
func predicateIsSubset(a interface{}, b ...interface{}) predicate.BoolPredicate {
return func() bool {
// Populate the set.
set := map[string]bool{}
for _, bval := range b {
s, ok := bval.(string)
if !ok {
return false
}
set[s] = true
}
switch aval := a.(type) {
case string:
return set[aval]
case []string:
for _, v := range aval {
if !set[v] {
return false
}
}
return true
default:
return false
}
}
}
// NewWhereParser returns standard parser for `where` section in access rules.
func NewWhereParser(ctx RuleContext) (predicate.Parser, error) {
return predicate.NewParser(predicate.Def{
@@ -62,8 +149,11 @@ func NewWhereParser(ctx RuleContext) (predicate.Parser, error) {
NOT: predicate.Not,
},
Functions: map[string]interface{}{
"equals": predicate.Equals,
"contains": predicate.Contains,
"equals": predicate.Equals,
"contains": predicate.Contains,
"all_end_with": predicateAllEndWith,
"all_equal": predicateAllEqual,
"is_subset": predicateIsSubset,
// system.catype is a function that returns cert authority type,
// it returns empty values for unrecognized values to
// pass static rule checks.
@@ -181,6 +271,8 @@ type Context struct {
Session events.AuditEvent
// SSHSession is an optional (active) SSH session.
SSHSession *session.Session
// HostCert is an optional host certificate.
HostCert *HostCertContext
}
// String returns user friendly representation of this context
@@ -205,6 +297,8 @@ const (
ImpersonateRoleIdentifier = "impersonate_role"
// ImpersonateUserIdentifier is a user to impersonate
ImpersonateUserIdentifier = "impersonate_user"
// HostCertIdentifier refers to a host certificate being created.
HostCertIdentifier = "host_cert"
)
// GetResource returns resource specified in the context,
@@ -246,6 +340,14 @@ func (ctx *Context) GetIdentifier(fields []string) (interface{}, error) {
// Do not expose the original session.Session, instead transform it into a
// ctxSession so the exposed fields match our desired API.
return predicate.GetFieldByTag(toCtxSession(ctx.SSHSession), teleport.JSON, fields[1:])
case HostCertIdentifier:
var hostCert *HostCertContext
if ctx.HostCert == nil {
hostCert = emptyHostCert
} else {
hostCert = ctx.HostCert
}
return predicate.GetFieldByTag(hostCert, teleport.JSON, fields[1:])
default:
return nil, trace.NotFound("%v is not defined", strings.Join(fields, "."))
}
@@ -293,12 +395,36 @@ func toCtxSession(s *session.Session) ctxSession {
}
}
// HostCertContext is used to evaluate the `where` condition on a `host_cert`
// pseudo-resource. These resources only exist for RBAC purposes and do not
// exist in the database.
type HostCertContext struct {
// HostID is the host ID in the cert request.
HostID string `json:"host_id"`
// NodeName is the node name in the cert request.
NodeName string `json:"node_name"`
// Principals is the list of requested certificate principals.
Principals []string `json:"principals"`
// ClusterName is the name of the cluster for which the certificate should
// be issued.
ClusterName string `json:"cluster_name"`
// Role is the name of the Teleport role for which the cert should be
// issued.
Role types.SystemRole `json:"role"`
// TTL is the requested certificate TTL.
TTL time.Duration `json:"ttl"`
}
// emptyResource is used when no resource is specified
var emptyResource = &EmptyResource{}
// emptyUser is used when no user is specified
var emptyUser = &types.UserV2{}
// emptyHostCert is an empty host certificate used when no host cert is
// specified
var emptyHostCert = &HostCertContext{}
// EmptyResource is used to represent a use case when no resource
// is specified in the rules matcher
type EmptyResource struct {
+84
View File
@@ -18,9 +18,11 @@ package services
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/vulcand/predicate"
"github.com/gravitational/teleport/api/types"
)
@@ -278,3 +280,85 @@ func TestNewResourceParser(t *testing.T) {
}
})
}
// TestParserHostCertContext tests set functions with a custom host cert
// context.
func TestParserHostCertContext(t *testing.T) {
t.Parallel()
for _, test := range []struct {
desc string
principals []string
positive []string
negative []string
}{
{
desc: "simple",
principals: []string{"foo.example.com"},
positive: []string{
`all_equal(host_cert.principals, "foo.example.com")`,
`is_subset(host_cert.principals, "a", "b", "foo.example.com")`,
`all_end_with(host_cert.principals, ".example.com")`,
},
negative: []string{
`all_equal(host_cert.principals, "foo")`,
`is_subset(host_cert.principals, "a", "b", "c")`,
`all_end_with(host_cert.principals, ".foo")`,
},
},
{
desc: "complex",
principals: []string{"node.foo.example.com", "node.bar.example.com"},
positive: []string{
`all_end_with(host_cert.principals, ".example.com")`,
`all_end_with(host_cert.principals, ".example.com") && !all_end_with(host_cert.principals, ".baz.example.com")`,
`equals(host_cert.host_id, "") && is_subset(host_cert.principals, "node.bar.example.com", "node.foo.example.com", "node.baz.example.com")`,
},
negative: []string{
`all_equal(host_cert.principals, "node.foo.example.com")`,
`all_end_with(host_cert.principals, ".foo.example.com") || all_end_with(host_cert.principals, ".bar.example.com")`,
`is_subset(host_cert.principals, "node.bar.example.com")`,
},
},
} {
ctx := Context{
User: &types.UserV2{},
HostCert: &HostCertContext{
HostID: "",
NodeName: "foo",
Principals: test.principals,
ClusterName: "example.com",
Role: types.RoleNode,
TTL: time.Minute * 20,
},
}
parser, err := NewWhereParser(&ctx)
require.NoError(t, err)
t.Run(test.desc, func(t *testing.T) {
t.Run("positive", func(t *testing.T) {
for _, pred := range test.positive {
expr, err := parser.Parse(pred)
require.NoError(t, err)
ret, ok := expr.(predicate.BoolPredicate)
require.True(t, ok)
require.True(t, ret(), pred)
}
})
t.Run("negative", func(t *testing.T) {
for _, pred := range test.negative {
expr, err := parser.Parse(pred)
require.NoError(t, err)
ret, ok := expr.(predicate.BoolPredicate)
require.True(t, ok)
require.False(t, ret(), pred)
}
})
})
}
}
+8
View File
@@ -23,6 +23,7 @@ import (
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/tbot/identity"
)
// Bot is an interface covering various public tbot.Bot methods to circumvent
@@ -42,4 +43,11 @@ type Bot interface {
// Client retrieves the current auth client.
Client() auth.ClientI
// AuthenticatedUserClientFromIdentity returns a client backed by a specific
// identity.
AuthenticatedUserClientFromIdentity(ctx context.Context, id *identity.Identity) (auth.ClientI, error)
// Config returns the current bot config
Config() *BotConfig
}
+256
View File
@@ -0,0 +1,256 @@
/*
Copyright 2022 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 config
import (
"context"
"testing"
"time"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
const (
// mockProxyAddr is the address of the mock proxy server, used in tests
mockProxyAddr = "tele.blackmesa.gov:443"
// mockClusterName is the cluster name for the mock auth client, used in
// tests
mockClusterName = "tele.blackmesa.gov"
)
// mockAuth is a minimal fake auth client, used in tests
type mockAuth struct {
auth.ClientI
clusterName string
proxyAddr string
t *testing.T
}
func (m *mockAuth) GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) {
cn, err := types.NewClusterName(types.ClusterNameSpecV2{
ClusterName: m.clusterName,
ClusterID: "aa-bb-cc",
})
require.NoError(m.t, err)
return cn, nil
}
func (m *mockAuth) Ping(ctx context.Context) (proto.PingResponse, error) {
require.NotNil(m.t, ctx)
return proto.PingResponse{
ProxyPublicAddr: m.proxyAddr,
}, nil
}
func (m *mockAuth) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool, opts ...services.MarshalOption) (types.CertAuthority, error) {
require.NotNil(m.t, ctx)
require.Equal(m.t, types.CertAuthID{
Type: types.HostCA,
DomainName: m.clusterName,
}, id)
require.False(m.t, loadKeys)
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.HostCA,
ClusterName: m.clusterName,
ActiveKeys: types.CAKeySet{
TLS: []*types.TLSKeyPair{
{
Cert: []byte(fixtures.TLSCACertPEM),
Key: []byte(fixtures.TLSCAKeyPEM),
},
},
SSH: []*types.SSHKeyPair{
// Two of these to ensure that both are written to known hosts
{
PrivateKey: []byte(fixtures.SSHCAPrivateKey),
PublicKey: []byte(fixtures.SSHCAPublicKey),
},
{
PrivateKey: []byte(fixtures.SSHCAPrivateKey),
PublicKey: []byte(fixtures.SSHCAPublicKey),
},
},
},
})
require.NoError(m.t, err)
return ca, nil
}
func (m *mockAuth) GetCertAuthorities(ctx context.Context, caType types.CertAuthType, loadKeys bool, opts ...services.MarshalOption) ([]types.CertAuthority, error) {
require.NotNil(m.t, ctx)
require.Equal(m.t, types.HostCA, caType)
require.False(m.t, loadKeys)
// We'll just wrap GetCertAuthority()'s dummy CA.
ca, err := m.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: m.clusterName,
}, loadKeys, opts...)
require.NoError(m.t, err)
return []types.CertAuthority{ca}, nil
}
func newMockAuth(t *testing.T) *mockAuth {
return &mockAuth{
t: t,
clusterName: mockClusterName,
proxyAddr: mockProxyAddr,
}
}
// mockBot is a minimal Bot impl that can be used in tests
type mockBot struct {
cfg *BotConfig
auth auth.ClientI
}
func (b *mockBot) AuthPing(ctx context.Context) (*proto.PingResponse, error) {
ping, err := b.auth.Ping(ctx)
if err != nil {
return nil, err
}
return &ping, err
}
func (b *mockBot) ProxyPing(ctx context.Context) (*webclient.PingResponse, error) {
return &webclient.PingResponse{}, nil
}
func (b *mockBot) GetCertAuthorities(ctx context.Context, caType types.CertAuthType) ([]types.CertAuthority, error) {
return b.auth.GetCertAuthorities(ctx, caType, false)
}
func (b *mockBot) Client() auth.ClientI {
return b.auth
}
func (b *mockBot) AuthenticatedUserClientFromIdentity(ctx context.Context, id *identity.Identity) (auth.ClientI, error) {
return nil, trace.NotImplemented("not implemented")
}
func (b *mockBot) Config() *BotConfig {
return b.cfg
}
func newMockBot(cfg *BotConfig, auth auth.ClientI) *mockBot {
return &mockBot{
cfg: cfg,
auth: auth,
}
}
// identRequest is a function used to add additional requests to an identity in
// getTestIdent.
type identRequest func(id *tlsca.Identity)
// kubernetesRequest requests a Kubernetes cluster.
func kubernetesRequest(k8sCluster string) identRequest {
return func(id *tlsca.Identity) {
id.KubernetesCluster = k8sCluster
}
}
// getTestIdent returns a mostly-valid bot Identity without starting up an
// entire Teleport server instance.
func getTestIdent(t *testing.T, username string, reqs ...identRequest) *identity.Identity {
ca, err := tlsca.FromKeys([]byte(fixtures.TLSCACertPEM), []byte(fixtures.TLSCAKeyPEM))
require.NoError(t, err)
privateKey, sshPublicKey, err := native.GenerateKeyPair()
require.NoError(t, err)
sshPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
require.NoError(t, err)
tlsPublicKeyPEM, err := tlsca.MarshalPublicKeyFromPrivateKeyPEM(sshPrivateKey)
require.NoError(t, err)
tlsPublicKey, err := tlsca.ParsePublicKeyPEM(tlsPublicKeyPEM)
require.NoError(t, err)
// Note: it'd be nice to make this more universally useful in our tests at
// some point.
clock := clockwork.NewFakeClock()
notAfter := clock.Now().Add(time.Hour)
id := tlsca.Identity{
Username: username,
KubernetesUsers: []string{"foo"},
KubernetesGroups: []string{"bar"},
RouteToCluster: mockClusterName,
}
for _, req := range reqs {
req(&id)
}
subject, err := id.Subject()
require.NoError(t, err)
certBytes, err := ca.GenerateCertificate(tlsca.CertificateRequest{
Clock: clock,
PublicKey: tlsPublicKey,
Subject: subject,
NotAfter: notAfter,
})
require.NoError(t, err)
caSigner, err := ssh.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey))
require.NoError(t, err)
ta := testauthority.New()
sshCertBytes, err := ta.GenerateUserCert(services.UserCertParams{
CASigner: caSigner,
PublicUserKey: sshPublicKey,
Username: username,
CertificateFormat: constants.CertificateFormatStandard,
TTL: time.Minute,
AllowedLogins: []string{"foo"},
RouteToCluster: mockClusterName,
})
require.NoError(t, err)
certs := &proto.Certs{
SSH: sshCertBytes,
TLS: certBytes,
TLSCACerts: [][]byte{[]byte(fixtures.TLSCACertPEM)},
SSHCACerts: [][]byte{[]byte(fixtures.SSHCAPublicKey)},
}
ident, err := identity.ReadIdentityFromStore(&identity.LoadIdentityParams{
PrivateKeyBytes: privateKey,
PublicKeyBytes: tlsPublicKeyPEM,
}, certs, identity.DestinationKinds()...)
require.NoError(t, err)
return ident
}
+17 -7
View File
@@ -58,6 +58,10 @@ const (
// TemplateKubernetesName is the config name for generating Kubernetes
// client config files
TemplateKubernetesName = "kubernetes"
// TemplateSSHHostCertName is the config name for generating SSH host
// certificates
TemplateSSHHostCertName = "ssh_host_cert"
)
// AllConfigTemplates lists all valid config templates, intended for help
@@ -70,6 +74,7 @@ var AllConfigTemplates = [...]string{
TemplateMongoName,
TemplateCockroachName,
TemplateKubernetesName,
TemplateSSHHostCertName,
}
// FileDescription is a minimal spec needed to create an empty end-user-owned
@@ -103,13 +108,14 @@ type Template interface {
// TemplateConfig contains all possible config template variants. Exactly one
// variant must be set to be considered valid.
type TemplateConfig struct {
SSHClient *TemplateSSHClient `yaml:"ssh_client,omitempty"`
Identity *TemplateIdentity `yaml:"identity,omitempty"`
TLS *TemplateTLS `yaml:"tls,omitempty"`
TLSCAs *TemplateTLSCAs `yaml:"tls_cas,omitempty"`
Mongo *TemplateMongo `yaml:"mongo,omitempty"`
Cockroach *TemplateCockroach `yaml:"cockroach,omitempty"`
Kubernetes *TemplateKubernetes `yaml:"kubernetes,omitempty"`
SSHClient *TemplateSSHClient `yaml:"ssh_client,omitempty"`
Identity *TemplateIdentity `yaml:"identity,omitempty"`
TLS *TemplateTLS `yaml:"tls,omitempty"`
TLSCAs *TemplateTLSCAs `yaml:"tls_cas,omitempty"`
Mongo *TemplateMongo `yaml:"mongo,omitempty"`
Cockroach *TemplateCockroach `yaml:"cockroach,omitempty"`
Kubernetes *TemplateKubernetes `yaml:"kubernetes,omitempty"`
SSHHostCert *TemplateSSHHostCert `yaml:"ssh_host_cert,omitempty"`
}
func (c *TemplateConfig) UnmarshalYAML(node *yaml.Node) error {
@@ -136,6 +142,8 @@ func (c *TemplateConfig) UnmarshalYAML(node *yaml.Node) error {
c.Cockroach = &TemplateCockroach{}
case TemplateKubernetesName:
c.Kubernetes = &TemplateKubernetes{}
case TemplateSSHHostCertName:
c.SSHHostCert = &TemplateSSHHostCert{}
default:
return trace.BadParameter(
"invalid config template '%s' on line %d, expected one of: %s",
@@ -160,6 +168,7 @@ func (c *TemplateConfig) CheckAndSetDefaults() error {
c.Mongo,
c.Cockroach,
c.Kubernetes,
c.SSHHostCert,
}
notNilCount := 0
@@ -200,6 +209,7 @@ func (c *TemplateConfig) GetConfigTemplate() (Template, error) {
c.Mongo,
c.Cockroach,
c.Kubernetes,
c.SSHHostCert,
}
for _, template := range templates {
@@ -22,181 +22,22 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
type templateKubernetesAuthMock struct {
auth.ClientI
clusterName string
t *testing.T
}
func (m *templateKubernetesAuthMock) GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) {
cn, err := types.NewClusterName(types.ClusterNameSpecV2{
ClusterName: m.clusterName,
ClusterID: "aa-bb-cc",
})
require.NoError(m.t, err)
return cn, nil
}
func (m *templateKubernetesAuthMock) Ping(ctx context.Context) (proto.PingResponse, error) {
require.NotNil(m.t, ctx)
return proto.PingResponse{
ProxyPublicAddr: "tele.blackmesa.gov:443",
}, nil
}
func (m *templateKubernetesAuthMock) GetCertAuthorities(ctx context.Context, caType types.CertAuthType, loadKeys bool, opts ...services.MarshalOption) ([]types.CertAuthority, error) {
require.NotNil(m.t, ctx)
require.Equal(m.t, types.HostCA, caType)
require.False(m.t, loadKeys)
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.HostCA,
ClusterName: m.clusterName,
ActiveKeys: types.CAKeySet{
TLS: []*types.TLSKeyPair{
{
Cert: []byte(fixtures.TLSCACertPEM),
Key: []byte(fixtures.TLSCAKeyPEM),
},
},
SSH: []*types.SSHKeyPair{
{
PrivateKey: []byte(fixtures.SSHCAPrivateKey),
PublicKey: []byte(fixtures.SSHCAPublicKey),
},
},
},
})
require.NoError(m.t, err)
return []types.CertAuthority{ca}, nil
}
type templateKubernetesMockBot struct {
mockAuth *templateKubernetesAuthMock
}
func (t *templateKubernetesMockBot) Client() auth.ClientI {
return t.mockAuth
}
func (t *templateKubernetesMockBot) AuthPing(ctx context.Context) (*proto.PingResponse, error) {
ping, err := t.mockAuth.Ping(ctx)
if err != nil {
return nil, err
}
return &ping, err
}
func (t *templateKubernetesMockBot) ProxyPing(ctx context.Context) (*webclient.PingResponse, error) {
return &webclient.PingResponse{}, nil
}
func (t *templateKubernetesMockBot) GetCertAuthorities(ctx context.Context, caType types.CertAuthType) ([]types.CertAuthority, error) {
return t.mockAuth.GetCertAuthorities(ctx, caType, false)
}
// getTestIdent returns a mostly-valid bot Identity without starting up an
// entire Teleport server instance.
func getTestIdent(t *testing.T, username string, k8sCluster string) *identity.Identity {
ca, err := tlsca.FromKeys([]byte(fixtures.TLSCACertPEM), []byte(fixtures.TLSCAKeyPEM))
require.NoError(t, err)
privateKey, sshPublicKey, err := native.GenerateKeyPair()
require.NoError(t, err)
sshPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
require.NoError(t, err)
tlsPublicKeyPEM, err := tlsca.MarshalPublicKeyFromPrivateKeyPEM(sshPrivateKey)
require.NoError(t, err)
tlsPublicKey, err := tlsca.ParsePublicKeyPEM(tlsPublicKeyPEM)
require.NoError(t, err)
// Note: it'd be nice to make this more universally useful in our tests at
// some point.
clock := clockwork.NewFakeClock()
notAfter := clock.Now().Add(time.Hour)
id := tlsca.Identity{
Username: username,
KubernetesUsers: []string{"foo"},
KubernetesGroups: []string{"bar"},
RouteToCluster: "teleport.localhost.localdomain",
KubernetesCluster: k8sCluster,
}
subject, err := id.Subject()
require.NoError(t, err)
certBytes, err := ca.GenerateCertificate(tlsca.CertificateRequest{
Clock: clock,
PublicKey: tlsPublicKey,
Subject: subject,
NotAfter: notAfter,
})
require.NoError(t, err)
caSigner, err := ssh.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey))
require.NoError(t, err)
ta := testauthority.New()
sshCertBytes, err := ta.GenerateUserCert(services.UserCertParams{
CASigner: caSigner,
PublicUserKey: sshPublicKey,
Username: username,
CertificateFormat: constants.CertificateFormatStandard,
TTL: time.Minute,
AllowedLogins: []string{"foo"},
RouteToCluster: "teleport.localhost.localdomain",
})
require.NoError(t, err)
certs := &proto.Certs{
SSH: sshCertBytes,
TLS: certBytes,
TLSCACerts: [][]byte{[]byte(fixtures.TLSCACertPEM)},
SSHCACerts: [][]byte{[]byte(fixtures.SSHCAPublicKey)},
}
ident, err := identity.ReadIdentityFromStore(&identity.LoadIdentityParams{
PrivateKeyBytes: privateKey,
PublicKeyBytes: tlsPublicKeyPEM,
}, certs, identity.DestinationKinds()...)
require.NoError(t, err)
return ident
}
// TestTemplateKubernetesRender renders a Kubernetes template and compares it
// to the saved golden result.
func TestTemplateKubernetesRender(t *testing.T) {
dir := t.TempDir()
mockAuth := &templateKubernetesAuthMock{
t: t,
clusterName: "teleport.localhost.localdomain",
}
mockBot := &templateKubernetesMockBot{
mockAuth: mockAuth,
}
mockAuth := newMockAuth(t)
cfg, err := NewDefaultConfig("example.com")
require.NoError(t, err)
mockBot := newMockBot(cfg, mockAuth)
template := TemplateKubernetes{
getExecutablePath: func() (string, error) {
return "tbot", nil
@@ -218,9 +59,9 @@ func TestTemplateKubernetesRender(t *testing.T) {
},
}
ident := getTestIdent(t, "bot-test", k8sCluster)
ident := getTestIdent(t, "bot-test", kubernetesRequest(k8sCluster))
err := template.Render(context.Background(), mockBot, ident, dest)
err = template.Render(context.Background(), mockBot, ident, dest)
require.NoError(t, err)
kubeconfigBytes, err := os.ReadFile(filepath.Join(dir, template.Path))
@@ -24,104 +24,20 @@ import (
"testing"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
type templateSSHClientAuthMock struct {
auth.ClientI
clusterName string
t *testing.T
}
func (m *templateSSHClientAuthMock) GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) {
cn, err := types.NewClusterName(types.ClusterNameSpecV2{
ClusterName: m.clusterName,
ClusterID: "aa-bb-cc",
})
require.NoError(m.t, err)
return cn, nil
}
func (m *templateSSHClientAuthMock) Ping(ctx context.Context) (proto.PingResponse, error) {
require.NotNil(m.t, ctx)
return proto.PingResponse{
ProxyPublicAddr: "tele.blackmesa.gov:443",
}, nil
}
func (m *templateSSHClientAuthMock) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool, opts ...services.MarshalOption) (types.CertAuthority, error) {
require.NotNil(m.t, ctx)
require.Equal(m.t, types.CertAuthID{
Type: types.HostCA,
DomainName: m.clusterName,
}, id)
require.False(m.t, loadKeys)
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.HostCA,
ClusterName: m.clusterName,
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{
// Two of these to ensure that both are written to known hosts
{
PrivateKey: []byte(fixtures.SSHCAPrivateKey),
PublicKey: []byte(fixtures.SSHCAPublicKey),
},
{
PrivateKey: []byte(fixtures.SSHCAPrivateKey),
PublicKey: []byte(fixtures.SSHCAPublicKey),
},
},
},
})
require.NoError(m.t, err)
return ca, nil
}
type templateSSHClientMockBot struct {
mockAuth *templateSSHClientAuthMock
}
func (t *templateSSHClientMockBot) Client() auth.ClientI {
return t.mockAuth
}
func (t *templateSSHClientMockBot) GetCertAuthorities(ctx context.Context, caType types.CertAuthType) ([]types.CertAuthority, error) {
return t.mockAuth.GetCertAuthorities(ctx, caType, false)
}
func (t *templateSSHClientMockBot) AuthPing(ctx context.Context) (*proto.PingResponse, error) {
ping, err := t.mockAuth.Ping(ctx)
if err != nil {
return nil, err
}
return &ping, err
}
func (t *templateSSHClientMockBot) ProxyPing(ctx context.Context) (*webclient.PingResponse, error) {
return nil, trace.NotImplemented("not implemented")
}
func TestTemplateSSHClient_Render(t *testing.T) {
dir := t.TempDir()
mockAuth := &templateSSHClientAuthMock{
t: t,
clusterName: "black-mesa",
}
mockBot := &templateSSHClientMockBot{
mockAuth: mockAuth,
}
mockAuth := newMockAuth(t)
cfg, err := NewDefaultConfig("example.com")
require.NoError(t, err)
mockBot := newMockBot(cfg, mockAuth)
template := TemplateSSHClient{
ProxyPort: 1337,
getSSHVersion: func() (*semver.Version, error) {
@@ -143,7 +59,7 @@ func TestTemplateSSHClient_Render(t *testing.T) {
},
}
err := template.Render(context.Background(), mockBot, ident, dest)
err = template.Render(context.Background(), mockBot, ident, dest)
require.NoError(t, err)
replaceTestDir := func(b []byte) []byte {
@@ -0,0 +1,134 @@
/*
Copyright 2022 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 config
import (
"context"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/identityfile"
"github.com/gravitational/teleport/lib/tbot/bot"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/gravitational/trace"
)
const (
// defaultSSHHostCertPrefix is the default filename prefix for the SSH host
// certificate
defaultSSHHostCertPrefix = "ssh_host"
)
// TemplateSSHHostCert contains parameters for the ssh_config config
// template
type TemplateSSHHostCert struct {
// Prefix is the filename prefix for the generated SSH host
// certificates
Prefix string `yaml:"prefix,omitempty"`
// Principals is a list of principals to request for the host cert.
Principals []string `yaml:"principals"`
}
// CheckAndSetDefaults validates a TemplateSSHHostCert.
func (c *TemplateSSHHostCert) CheckAndSetDefaults() error {
if c.Prefix == "" {
c.Prefix = defaultSSHHostCertPrefix
}
if len(c.Principals) == 0 {
return trace.BadParameter("at least one principal must be specified")
}
return nil
}
// Name returns the name for the ssh_host_cert template.
func (c *TemplateSSHHostCert) Name() string {
return TemplateSSHHostCertName
}
// Describe lists the files to be generated by the ssh_host_cert template.
func (c *TemplateSSHHostCert) Describe(destination bot.Destination) []FileDescription {
ret := []FileDescription{
{
Name: c.Prefix,
},
{
Name: c.Prefix + "-cert.pub",
},
}
return ret
}
// Render generates SSH host cert files.
func (c *TemplateSSHHostCert) Render(ctx context.Context, bot Bot, currentIdentity *identity.Identity, destination *DestinationConfig) error {
dest, err := destination.GetDestination()
if err != nil {
return trace.Wrap(err)
}
// We'll need a client for the impersonated identity to request the certs.
authClient, err := bot.AuthenticatedUserClientFromIdentity(ctx, currentIdentity)
if err != nil {
return trace.Wrap(err)
}
cn, err := authClient.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
clusterName := cn.GetClusterName()
// generate a keypair
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
// For now, we'll reuse the bot's regular TTL, and hostID and nodeName are
// left unset.
botCfg := bot.Config()
key.Cert, err = authClient.GenerateHostCert(key.MarshalSSHPublicKey(),
"", "", c.Principals,
clusterName, types.RoleNode, botCfg.CertificateTTL)
if err != nil {
return trace.Wrap(err)
}
cfg := identityfile.WriteConfig{
OutputPath: c.Prefix,
Writer: &BotConfigWriter{
dest: dest,
},
Key: key,
Format: identityfile.FormatOpenSSH,
// Always overwrite to avoid hitting our no-op Stat() and Remove() functions.
OverwriteDestination: true,
}
files, err := identityfile.Write(cfg)
if err != nil {
return trace.Wrap(err)
}
log.Debugf("Wrote OpenSSH host cert files: %+v", files)
return nil
}
@@ -0,0 +1,91 @@
/*
Copyright 2022 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 config
import (
"context"
"testing"
"time"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/stretchr/testify/require"
)
type mockHostCertAuth struct {
mockAuth
}
func (m *mockHostCertAuth) GenerateHostCert(
key []byte, hostID, nodeName string, principals []string,
clusterName string, role types.SystemRole, ttl time.Duration,
) ([]byte, error) {
// We could generate a cert easily enough here, but the template generates a
// random key each run so the resulting cert will change too.
// The CA fixture isn't even a cert but we never examine it, so it'll do the
// job.
return []byte(fixtures.SSHCAPublicKey), nil
}
type mockHostCertBot struct {
mockBot
}
func (b *mockHostCertBot) AuthenticatedUserClientFromIdentity(
ctx context.Context, id *identity.Identity,
) (auth.ClientI, error) {
// For our purposes here, the mock client is sufficient.
return b.auth, nil
}
func TestTemplateSSHHostCertRender(t *testing.T) {
mockAuth := &mockHostCertAuth{
mockAuth: *newMockAuth(t),
}
cfg, err := NewDefaultConfig("example.com")
require.NoError(t, err)
mockBot := &mockHostCertBot{
mockBot: *newMockBot(cfg, mockAuth),
}
template := TemplateSSHHostCert{
Prefix: "example",
Principals: []string{"foo.example.com"},
}
require.NoError(t, template.CheckAndSetDefaults())
memory := &DestinationMemory{}
dest := &DestinationConfig{
DestinationMixin: DestinationMixin{
Memory: memory,
},
}
require.NoError(t, dest.CheckAndSetDefaults())
ident := getTestIdent(t, "bot-test")
err = template.Render(context.Background(), mockBot, ident, dest)
require.NoError(t, err)
certBytes, err := memory.Read(template.Prefix + "-cert.pub")
require.NoError(t, err)
require.Equal(t, fixtures.SSHCAPublicKey, string(certBytes))
}
@@ -3,17 +3,17 @@ clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
server: https://tele.blackmesa.gov:443
name: teleport.localhost.localdomain-example
name: tele.blackmesa.gov-example
contexts:
- context:
cluster: teleport.localhost.localdomain-example
user: teleport.localhost.localdomain-example
name: teleport.localhost.localdomain-example
current-context: teleport.localhost.localdomain-example
cluster: tele.blackmesa.gov-example
user: tele.blackmesa.gov-example
name: tele.blackmesa.gov-example
current-context: tele.blackmesa.gov-example
kind: Config
preferences: {}
users:
- name: teleport.localhost.localdomain-example
- name: tele.blackmesa.gov-example
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
@@ -1,2 +1,2 @@
@cert-authority tele.blackmesa.gov,black-mesa,*.black-mesa ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host
@cert-authority tele.blackmesa.gov,black-mesa,*.black-mesa ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host
@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host
@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host
@@ -1,17 +1,17 @@
# Begin generated Teleport configuration for tele.blackmesa.gov by tbot
# Common flags for all black-mesa hosts
Host *.black-mesa tele.blackmesa.gov
# Common flags for all tele.blackmesa.gov hosts
Host *.tele.blackmesa.gov tele.blackmesa.gov
UserKnownHostsFile "/test/dir/known_hosts"
IdentityFile "/test/dir/key"
CertificateFile "/test/dir/key-cert.pub"
HostKeyAlgorithms [email protected]
PubkeyAcceptedAlgorithms [email protected]
# Flags for all black-mesa hosts except the proxy
Host *.black-mesa !tele.blackmesa.gov
# Flags for all tele.blackmesa.gov hosts except the proxy
Host *.tele.blackmesa.gov !tele.blackmesa.gov
Port 3022
ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy=tele.blackmesa.gov ssh --cluster=black-mesa %r@%h:%p
ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy=tele.blackmesa.gov ssh --cluster=tele.blackmesa.gov %r@%h:%p
# End generated Teleport configuration
+6 -8
View File
@@ -64,11 +64,11 @@ func generateKeys() (private, sshpub, tlspub []byte, err error) {
return privateKey, publicKey, tlsPublicKey, nil
}
// authenticatedUserClientFromIdentity creates a new auth client from the given
// AuthenticatedUserClientFromIdentity creates a new auth client from the given
// identity. Note that depending on the connection address given, this may
// attempt to connect via the proxy and therefore requires both SSH and TLS
// credentials.
func (b *Bot) authenticatedUserClientFromIdentity(ctx context.Context, id *identity.Identity, authServer string) (auth.ClientI, error) {
func (b *Bot) AuthenticatedUserClientFromIdentity(ctx context.Context, id *identity.Identity) (auth.ClientI, error) {
if id.SSHCert == nil || id.X509Cert == nil {
return nil, trace.BadParameter("auth client requires a fully formed identity")
}
@@ -83,7 +83,7 @@ func (b *Bot) authenticatedUserClientFromIdentity(ctx context.Context, id *ident
return nil, trace.Wrap(err)
}
authAddr, err := utils.ParseAddr(authServer)
authAddr, err := utils.ParseAddr(b.cfg.AuthServer)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -371,7 +371,7 @@ func (b *Bot) generateImpersonatedIdentity(
// Now that we have an initial impersonated identity, we can use it to
// request any app/db/etc certs
if destCfg.Database != nil {
impClient, err := b.authenticatedUserClientFromIdentity(ctx, ident, b.cfg.AuthServer)
impClient, err := b.AuthenticatedUserClientFromIdentity(ctx, ident)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -406,7 +406,7 @@ func (b *Bot) generateImpersonatedIdentity(
return newIdent, trace.Wrap(err)
} else if destCfg.App != nil {
impClient, err := b.authenticatedUserClientFromIdentity(ctx, ident, b.cfg.AuthServer)
impClient, err := b.AuthenticatedUserClientFromIdentity(ctx, ident)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -563,9 +563,7 @@ func (b *Bot) renew(
// Immediately attempt to reconnect using the new identity (still
// haven't persisted the known-good certs).
newClient, err := b.authenticatedUserClientFromIdentity(
ctx, newIdentity, b.cfg.AuthServer,
)
newClient, err := b.AuthenticatedUserClientFromIdentity(ctx, newIdentity)
if err != nil {
return trace.Wrap(err)
}
+7 -2
View File
@@ -65,6 +65,11 @@ func New(cfg *config.BotConfig, log logrus.FieldLogger, reloadChan chan struct{}
}
}
// Config returns the current bot config
func (b *Bot) Config() *config.BotConfig {
return b.cfg
}
// Client retrieves the current auth client.
func (b *Bot) Client() auth.ClientI {
b.mu.Lock()
@@ -298,7 +303,7 @@ func (b *Bot) initialize(ctx context.Context) error {
b.log.Warn("Note: onboarding config ignored as identity was loaded from persistent storage")
}
authClient, err = b.authenticatedUserClientFromIdentity(ctx, ident, b.cfg.AuthServer)
authClient, err = b.AuthenticatedUserClientFromIdentity(ctx, ident)
if err != nil {
return trace.Wrap(err)
}
@@ -329,7 +334,7 @@ func (b *Bot) initialize(ctx context.Context) error {
}
b.log.Debug("Attempting first connection using initial auth client")
authClient, err = b.authenticatedUserClientFromIdentity(ctx, ident, b.cfg.AuthServer)
authClient, err = b.AuthenticatedUserClientFromIdentity(ctx, ident)
if err != nil {
return trace.Wrap(err)
}