mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
+6
-6
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user