Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode Join Attributes in Bot Certificates #49426

Merged
merged 18 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/gen/proto/go/teleport/machineid/v1/bot_instance.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions api/proto/teleport/machineid/v1/bot_instance.proto
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,16 @@ message BotInstanceStatusAuthentication {
// Server.
google.protobuf.Timestamp authenticated_at = 1;
// The join method used for this join or renewal.
// Deprecated: prefer using join_attrs.meta.join_method
string join_method = 2;
// The join token used for this join or renewal. This is only populated for
// delegated join methods as the value for `token` join methods is sensitive.
// Deprecated: prefer using join_attrs.meta.join_token_name
string join_token = 3;
// The metadata sourced from the join method.
// Deprecated: prefer using join_attrs.
google.protobuf.Struct metadata = 4;

// On each renewal, this generation is incremented. For delegated join
// methods, this counter is not checked during renewal. For the `token` join
// method, this counter is checked during renewal and the Bot is locked out if
Expand Down
7 changes: 6 additions & 1 deletion lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import (
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -2286,6 +2287,9 @@ type certRequest struct {
// botInstanceID is the unique identifier of the bot instance associated
// with this cert, if any
botInstanceID string
// joinAttributes holds attributes derived from attested metadata from the
// join process, should any exist.
joinAttributes *workloadidentityv1pb.JoinAttrs
}

// check verifies the cert request is valid.
Expand Down Expand Up @@ -3366,7 +3370,8 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
AssetTag: req.deviceExtensions.AssetTag,
CredentialID: req.deviceExtensions.CredentialID,
},
UserType: req.user.GetUserType(),
UserType: req.user.GetUserType(),
JoinAttributes: req.joinAttributes,
}

var signedTLSCert []byte
Expand Down
3 changes: 3 additions & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,9 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
// `updateBotInstance()` is called below, and this (empty) value will be
// overridden.
botInstanceID: a.context.Identity.GetIdentity().BotInstanceID,
// Propagate any join attributes from the current identity to the new
// identity.
joinAttributes: a.context.Identity.GetIdentity().JoinAttributes,
}

if user.GetName() != a.context.User.GetName() {
Expand Down
25 changes: 14 additions & 11 deletions lib/auth/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
apiutils "github.com/gravitational/teleport/api/utils"
Expand Down Expand Up @@ -315,7 +316,7 @@ func (a *Server) updateBotInstance(
if templateAuthRecord != nil {
authRecord.JoinToken = templateAuthRecord.JoinToken
authRecord.JoinMethod = templateAuthRecord.JoinMethod
authRecord.Metadata = templateAuthRecord.Metadata
authRecord.JoinAttrs = templateAuthRecord.JoinAttrs
}

// An empty bot instance most likely means a bot is rejoining after an
Expand Down Expand Up @@ -493,6 +494,7 @@ func (a *Server) generateInitialBotCerts(
expires time.Time, renewable bool,
initialAuth *machineidv1pb.BotInstanceStatusAuthentication,
existingInstanceID string, currentIdentityGeneration int32,
joinAttrs *workloadidentityv1pb.JoinAttrs,
) (*proto.Certs, string, error) {
var err error

Expand Down Expand Up @@ -535,16 +537,17 @@ func (a *Server) generateInitialBotCerts(

// Generate certificate
certReq := certRequest{
user: userState,
ttl: expires.Sub(a.GetClock().Now()),
sshPublicKey: sshPubKey,
tlsPublicKey: tlsPubKey,
checker: checker,
traits: accessInfo.Traits,
renewable: renewable,
includeHostCA: true,
loginIP: loginIP,
botName: botName,
user: userState,
ttl: expires.Sub(a.GetClock().Now()),
sshPublicKey: sshPubKey,
tlsPublicKey: tlsPubKey,
checker: checker,
traits: accessInfo.Traits,
renewable: renewable,
includeHostCA: true,
loginIP: loginIP,
botName: botName,
joinAttributes: joinAttrs,
}

if existingInstanceID == "" {
Expand Down
144 changes: 143 additions & 1 deletion lib/auth/bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
"google.golang.org/protobuf/testing/protocmp"

"github.com/gravitational/teleport"
apiclient "github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/client/webclient"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/integrations/lib/testing/fakejoin"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/join"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
Expand Down Expand Up @@ -216,6 +219,146 @@ func TestRegisterBotCertificateGenerationCheck(t *testing.T) {
}
}

// TestBotJoinAttrs_Kubernetes validates that a bot can join using the
// Kubernetes join method and that the correct join attributes are encoded in
// the resulting bot cert, and, that when this cert is used to produce role
// certificates, the correct attributes are encoded in the role cert.
//
// Whilst this specifically tests the Kubernetes join method, it tests by proxy
// the implementation for most of the join methods.
func TestBotJoinAttrs_Kubernetes(t *testing.T) {
t.Parallel()

srv := newTestTLSServer(t)
ctx := context.Background()

role, err := CreateRole(ctx, srv.Auth(), "example", types.RoleSpecV6{})
require.NoError(t, err)

// Create a new bot.
client, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
bot, err := client.BotServiceClient().CreateBot(ctx, &machineidv1pb.CreateBotRequest{
Bot: &machineidv1pb.Bot{
Metadata: &headerv1.Metadata{
Name: "test",
},
Spec: &machineidv1pb.BotSpec{
Roles: []string{"example"},
},
},
})
require.NoError(t, err)

k8s, err := fakejoin.NewKubernetesSigner(srv.Clock())
require.NoError(t, err)
jwks, err := k8s.GetMarshaledJWKS()
require.NoError(t, err)
fakePSAT, err := k8s.SignServiceAccountJWT(
"my-pod",
"my-namespace",
"my-service-account",
srv.ClusterName(),
)
require.NoError(t, err)

tok, err := types.NewProvisionTokenFromSpec(
"my-k8s-token",
time.Time{},
types.ProvisionTokenSpecV2{
Roles: types.SystemRoles{types.RoleBot},
JoinMethod: types.JoinMethodKubernetes,
BotName: bot.Metadata.Name,
Kubernetes: &types.ProvisionTokenSpecV2Kubernetes{
Type: types.KubernetesJoinTypeStaticJWKS,
StaticJWKS: &types.ProvisionTokenSpecV2Kubernetes_StaticJWKSConfig{
JWKS: jwks,
},
Allow: []*types.ProvisionTokenSpecV2Kubernetes_Rule{
{
ServiceAccount: "my-namespace:my-service-account",
},
},
},
},
)
require.NoError(t, err)
require.NoError(t, client.CreateToken(ctx, tok))

result, err := join.Register(ctx, join.RegisterParams{
Token: tok.GetName(),
JoinMethod: types.JoinMethodKubernetes,
ID: state.IdentityID{
Role: types.RoleBot,
},
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
KubernetesReadFileFunc: func(name string) ([]byte, error) {
return []byte(fakePSAT), nil
},
})
require.NoError(t, err)

// Validate correct join attributes are encoded.
cert, err := tlsca.ParseCertificatePEM(result.Certs.TLS)
require.NoError(t, err)
ident, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
wantAttrs := &workloadidentityv1pb.JoinAttrs{
Meta: &workloadidentityv1pb.JoinAttrsMeta{
JoinTokenName: tok.GetName(),
JoinMethod: string(types.JoinMethodKubernetes),
},
Kubernetes: &workloadidentityv1pb.JoinAttrsKubernetes{
ServiceAccount: &workloadidentityv1pb.JoinAttrsKubernetesServiceAccount{
Namespace: "my-namespace",
Name: "my-service-account",
},
Pod: &workloadidentityv1pb.JoinAttrsKubernetesPod{
Name: "my-pod",
},
Subject: "system:serviceaccount:my-namespace:my-service-account",
},
}
require.Empty(t, cmp.Diff(
ident.JoinAttributes,
wantAttrs,
protocmp.Transform(),
))

// Now, try to produce a role certificate using the bot cert, to ensure
// that the join attributes are correctly propagated.
privateKeyPEM, err := keys.MarshalPrivateKey(result.PrivateKey)
require.NoError(t, err)
tlsCert, err := tls.X509KeyPair(result.Certs.TLS, privateKeyPEM)
require.NoError(t, err)
sshPub, err := ssh.NewPublicKey(result.PrivateKey.Public())
require.NoError(t, err)
tlsPub, err := keys.MarshalPublicKey(result.PrivateKey.Public())
require.NoError(t, err)
botClient := srv.NewClientWithCert(tlsCert)
roleCerts, err := botClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: ssh.MarshalAuthorizedKey(sshPub),
TLSPublicKey: tlsPub,
Username: bot.Status.UserName,
RoleRequests: []string{
role.GetName(),
},
UseRoleRequests: true,
Expires: srv.Clock().Now().Add(time.Hour),
})
require.NoError(t, err)

roleCert, err := tlsca.ParseCertificatePEM(roleCerts.TLS)
require.NoError(t, err)
roleIdent, err := tlsca.FromSubject(roleCert.Subject, roleCert.NotAfter)
require.NoError(t, err)
require.Empty(t, cmp.Diff(
roleIdent.JoinAttributes,
wantAttrs,
protocmp.Transform(),
))
}

// TestRegisterBotInstance tests that bot instances are created properly on join
func TestRegisterBotInstance(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -282,7 +425,6 @@ func TestRegisterBotInstance(t *testing.T) {
require.Equal(t, int32(1), ia.Generation)
require.Equal(t, string(types.JoinMethodToken), ia.JoinMethod)
require.Equal(t, token.GetSafeName(), ia.JoinToken)

// The latest authentications field should contain the same record (and
// only that record.)
require.Len(t, botInstance.GetStatus().LatestAuthentications, 1)
Expand Down
Loading
Loading