Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,15 @@ func (c *Client) UnstableAssertSystemRole(ctx context.Context, req proto.Unstabl
return trace.Wrap(err)
}

// AssertSystemRole is used by agents to prove that they have a given system role when their credentials originate
// from multiple separate join tokens so that they can be issued an instance certificate that encompasses
// all of their capabilities. This method will be deprecated once we have a more comprehensive
// model for join token joining/replacement.
func (c *Client) AssertSystemRole(ctx context.Context, req proto.SystemRoleAssertion) error {
_, err := c.grpc.AssertSystemRole(ctx, &req)
return trace.Wrap(err)
}

// EmitAuditEvent sends an auditable event to the auth server.
func (c *Client) EmitAuditEvent(ctx context.Context, event events.AuditEvent) error {
grpcEvent, err := events.ToOneOf(event)
Expand Down
2,448 changes: 1,568 additions & 880 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ message HostCertsRequest {
// system roles are held.
// DELETE IN: 12.0 (deprecated in v11, but required for back-compat with v10 clients)
string UnstableSystemRoleAssertionID = 12 [(gogoproto.jsontag) = "system_role_assertion_id,omitempty"];

// SystemRoleAssertionID is used by agents to prove that they have a given system role when
// their credentials originate from multiple separate join tokens so that they can be issued
// an instance certificate that encompasses all of their capabilities. This field will be
// deprecated once we have a more comprehensive model for join token joining/replacement.
string SystemRoleAssertionID = 13 [(gogoproto.jsontag) = "system_role_assertion_id,omitempty"];
}

// OpenSSHCertRequest specifies certificate-generation parameters
Expand Down Expand Up @@ -1983,6 +1989,25 @@ message UnstableSystemRoleAssertion {
];
}

// SystemRoleAssertion is used by agents to prove that they have a given system role when their
// credentials originate from multiple separate join tokens so that they can be issued an
// instance certificate that encompasses all of their capabilities. This type will be
// deprecated once we have a more comprehensive model for join token joining/replacement.
message SystemRoleAssertion {
// ServerID is the server ID of the instance that the assertion is for. Assertions are
// only accepted if the calling agent's certificate matches this server id.
string ServerID = 1 [(gogoproto.jsontag) = "server_id,omitempty"];
// AssertionID is a random UUID that uniquely identifies a set of assertions
// as originating from the same teleport process.
string AssertionID = 2 [(gogoproto.jsontag) = "assertion_id,omitempty"];
// SystemRole is the system role being asserted. Assertions are only accepted if
// the calling agent's certificate authorizes it for this system role.
string SystemRole = 3 [
(gogoproto.jsontag) = "system_role,omitempty",
(gogoproto.casttype) = "github.com/gravitational/teleport/api/types.SystemRole"
];
}

// UnstableSystemRoleAssertionSet is not a stable part of the public API. Records the sum of system
// role assertions provided by a given instance.
// DELETE IN: 12.0 (deprecated in v11, but required for back-compat with v10 clients)
Expand All @@ -1995,6 +2020,22 @@ message UnstableSystemRoleAssertionSet {
];
}

// SystemRoleAssertionSet is an aggregate generated as a result of one or more successful
// assertions. This type will be deprecated once we have a more comprehensive model for
// join token joining/replacement.
message SystemRoleAssertionSet {
// ServerID is the server ID of the agent that generated the assertions.
string ServerID = 1 [(gogoproto.jsontag) = "server_id,omitempty"];
// AssertionID is a random UUID that identified all constituent assertions as originating
// from the same teleport process.
string AssertionID = 2 [(gogoproto.jsontag) = "assertion_id,omitempty"];
// SystemRoles is the set of system roles that the agent has successfully asserted.
repeated string SystemRoles = 3 [
(gogoproto.jsontag) = "system_roles,omitempty",
(gogoproto.casttype) = "github.com/gravitational/teleport/api/types.SystemRole"
];
}

// UpstreamInventoryOneOf is the upstream message for the inventory control stream,
// sent from teleport instances to the auth server.
message UpstreamInventoryOneOf {
Expand Down Expand Up @@ -2949,6 +2990,12 @@ service AuthService {
// DELETE IN: 12.0 (deprecated in v11, but required for back-compat with v10 clients)
rpc UnstableAssertSystemRole(UnstableSystemRoleAssertion) returns (google.protobuf.Empty);

// AssertSystemRole is used by agents to prove that they have a given system role when their
// credentials originate from multiple separate join tokens so that they can be issued an instance
// certificate that encompasses all of their capabilities. This method will be deprecated once we
// have a more comprehensive model for join token joining/replacement.
rpc AssertSystemRole(SystemRoleAssertion) returns (google.protobuf.Empty);

// SubmitUsageEvent submits an external usage event.
rpc SubmitUsageEvent(SubmitUsageEventRequest) returns (google.protobuf.Empty);

Expand Down
207 changes: 207 additions & 0 deletions integration/instance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package integration

import (
"context"
"net"
"os"
"path/filepath"
"testing"
"time"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/breaker"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)

// basicDirCopy performs a very simplistic recursive copy from one directory to another. this helper was
// written specifically for setting up teleport data directories for testing purposes and may not be
// suitable for other applications.
func basicDirCopy(src string, dst string) error {
entries, err := os.ReadDir(src)
if err != nil {
return trace.Wrap(err)
}

if err := os.MkdirAll(dst, teleport.PrivateDirMode); err != nil {
return trace.Wrap(err)
}

for _, entry := range entries {
if entry.IsDir() {
if err := basicDirCopy(filepath.Join(src, entry.Name()), filepath.Join(dst, entry.Name())); err != nil {
return trace.Wrap(err)
}
continue
}

data, err := os.ReadFile(filepath.Join(src, entry.Name()))
if err != nil {
return trace.Wrap(err)
}

if err := os.WriteFile(filepath.Join(dst, entry.Name()), data, teleport.PrivateDirMode); err != nil {
return trace.Wrap(err)
}
}

return nil
}

func getFreeListenAddr() (string, error) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
return "", trace.Wrap(err)
}

defer l.Close()
return l.Addr().String(), nil
}

// TestInstanceCertReissue tests the reissuance of an instance certificate when
// the instance has malformed system roles using pre-constructed data directories
// generated by an older teleport version that permitted token mix-and-match.
func TestInstanceCertReissue(t *testing.T) {
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Create temporary directories for the auth and agent data directories.
authDir, agentDir := t.TempDir(), t.TempDir()

// Write the instance assets to the temporary directories to set up pre-existing
// state for our teleport instances to use.
require.NoError(t, basicDirCopy("testdata/auth", authDir))
require.NoError(t, basicDirCopy("testdata/agent", agentDir))

proxyAddr, err := getFreeListenAddr()
require.NoError(t, err)

authAddr, err := getFreeListenAddr()
require.NoError(t, err)

authCfg := servicecfg.MakeDefaultConfig()
authCfg.Version = defaults.TeleportConfigVersionV3
authCfg.DataDir = authDir
require.NoError(t, authCfg.SetAuthServerAddresses([]utils.NetAddr{
{
AddrNetwork: "tcp",
Addr: authAddr,
},
}))
authCfg.Auth.Enabled = true
// ensure auth server is using the pre-constructed sqlite db
authCfg.Auth.StorageConfig.Params = backend.Params{defaults.BackendPath: filepath.Join(authDir, defaults.BackendDir)}
authCfg.Auth.ClusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
ClusterName: "auth-server",
})
require.NoError(t, err)
authCfg.Auth.ListenAddr.Addr = authAddr
authCfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)

authCfg.Proxy.Enabled = true
authCfg.Proxy.DisableWebInterface = true
authCfg.Proxy.WebAddr.Addr = proxyAddr

authCfg.SSH.Enabled = true
authCfg.SSH.Addr.Addr = "localhost:0"
authCfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
authCfg.Log = utils.NewLoggerForTests()

authRunErrCh := make(chan error, 1)
go func() {
authRunErrCh <- service.Run(ctx, *authCfg, func(cfg *servicecfg.Config) (service.Process, error) {
proc, err := service.NewTeleport(cfg)
if err != nil {
return nil, trace.Wrap(err)
}
return proc, nil
})
}()

agentCfg := servicecfg.MakeDefaultConfig()
agentCfg.Version = defaults.TeleportConfigVersionV3
agentCfg.DataDir = agentDir
agentCfg.ProxyServer = utils.NetAddr{
AddrNetwork: "tcp",
Addr: proxyAddr,
}

agentCfg.Auth.Enabled = false
agentCfg.Proxy.Enabled = false
agentCfg.SSH.Enabled = true

agentCfg.WindowsDesktop.Enabled = true
agentCfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
agentCfg.Log = utils.NewLoggerForTests()
agentCfg.MaxRetryPeriod = time.Second

agentRunErrCh := make(chan error, 1)
agentIdentitiesCh := make(chan *auth.Identity, 2)
go func() {
agentRunErrCh <- service.Run(ctx, *agentCfg, func(cfg *servicecfg.Config) (service.Process, error) {
proc, err := service.NewTeleport(cfg)
if err != nil {
return nil, trace.Wrap(err)
}

identity, err := proc.GetIdentity(types.RoleInstance)
if err != nil {
proc.Close()
return nil, trace.Wrap(err)
}

select {
case agentIdentitiesCh <- identity:
default:
}

return proc, nil
})
}()

timeout := time.After(time.Second * 30)
select {
case <-timeout:
t.Fatal("timed out waiting for first agent identity")
case identity := <-agentIdentitiesCh:
require.ElementsMatch(t, []string{string(types.RoleNode)}, identity.SystemRoles)
}

select {
case <-timeout:
t.Fatal("timed out waiting for second agent identity")
case identity := <-agentIdentitiesCh:
require.ElementsMatch(t, []string{string(types.RoleNode), string(types.RoleWindowsDesktop)}, identity.SystemRoles)
}
}
1 change: 1 addition & 0 deletions integration/testdata/agent/host_uuid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
57e20920-f2d0-439d-972b-0af9372b851d
Binary file added integration/testdata/agent/proc/sqlite.db
Binary file not shown.
Binary file added integration/testdata/auth/backend/sqlite.db
Binary file not shown.
1 change: 1 addition & 0 deletions integration/testdata/auth/host_uuid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
85c53d95-8da5-4200-a6af-6e7c633d4780
Binary file added integration/testdata/auth/proc/sqlite.db
Binary file not shown.
14 changes: 9 additions & 5 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3812,14 +3812,18 @@ func (a *Server) GenerateHostCerts(ctx context.Context, req *proto.HostCertsRequ
}, nil
}

// UnstableAssertSystemRole is not a stable part of the public API. Used by older
// instances to prove that they hold a given system role.
// DELETE IN: 12.0 (deprecated in v11, but required for back-compat with v10 clients)
func (a *Server) UnstableAssertSystemRole(ctx context.Context, req proto.UnstableSystemRoleAssertion) error {
// AssertSystemRole is used by agents to prove that they have a given system role when their credentials
// originate from multiple separate join tokens so that they can be issued an instance certificate that
// encompasses all of their capabilities. This method will be deprecated once we have a more comprehensive
// model for join token joining/replacement.
func (a *Server) AssertSystemRole(ctx context.Context, req proto.SystemRoleAssertion) error {
return trace.Wrap(a.Unstable.AssertSystemRole(ctx, req))
}

func (a *Server) UnstableGetSystemRoleAssertions(ctx context.Context, serverID string, assertionID string) (proto.UnstableSystemRoleAssertionSet, error) {
// GetSystemRoleAssertions is used in validated claims made by older instances to prove that they hold a given
// system role. This method will be deprecated once we have a more comprehensive model for join token
// joining/replacement.
func (a *Server) GetSystemRoleAssertions(ctx context.Context, serverID string, assertionID string) (proto.SystemRoleAssertionSet, error) {
set, err := a.Unstable.GetSystemRoleAssertions(ctx, serverID, assertionID)
return set, trace.Wrap(err)
}
Expand Down
28 changes: 22 additions & 6 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,15 +951,19 @@ func (a *ServerWithRoles) checkAdditionalSystemRoles(ctx context.Context, req *p
}

// load system role assertions if relevant
var assertions proto.UnstableSystemRoleAssertionSet
var assertions proto.SystemRoleAssertionSet
var err error
if req.UnstableSystemRoleAssertionID != "" {
assertions, err = a.authServer.UnstableGetSystemRoleAssertions(ctx, req.HostID, req.UnstableSystemRoleAssertionID)
assertionID := req.UnstableSystemRoleAssertionID
if req.SystemRoleAssertionID != "" {
assertionID = req.SystemRoleAssertionID
}
if assertionID != "" {
assertions, err = a.authServer.GetSystemRoleAssertions(ctx, req.HostID, assertionID)
if err != nil {
// include this error in the logs, since it might be indicative of a bug if it occurs outside of the context
// of a general backend outage.
log.Warnf("Failed to load system role assertion set %q for instance %q: %v", req.UnstableSystemRoleAssertionID, req.HostID, err)
return trace.AccessDenied("failed to load system role assertion set with ID %q", req.UnstableSystemRoleAssertionID)
log.Warnf("Failed to load system role assertion set %q for instance %q: %v", assertionID, req.HostID, err)
return trace.AccessDenied("failed to load system role assertion set with ID %q", assertionID)
}
}

Expand All @@ -985,6 +989,18 @@ Outer:
}

func (a *ServerWithRoles) UnstableAssertSystemRole(ctx context.Context, req proto.UnstableSystemRoleAssertion) error {
return trace.Wrap(a.AssertSystemRole(ctx, proto.SystemRoleAssertion{
ServerID: req.ServerID,
AssertionID: req.AssertionID,
SystemRole: req.SystemRole,
}))
}

// AssertSystemRole is used by agents to prove that they have a given system role when their credentials
// originate from multiple separate join tokens so that they can be issued an instance certificate that
// encompasses all of their capabilities. This method will be deprecated once we have a more comprehensive
// model for join token joining/replacement.
func (a *ServerWithRoles) AssertSystemRole(ctx context.Context, req proto.SystemRoleAssertion) error {
role, ok := a.context.Identity.(authz.BuiltinRole)
if !ok || !role.IsServer() {
return trace.AccessDenied("system role assertions can only be executed by a teleport built-in server")
Expand All @@ -1002,7 +1018,7 @@ func (a *ServerWithRoles) UnstableAssertSystemRole(ctx context.Context, req prot
return trace.AccessDenied("cannot assert non-service system role %q", req.SystemRole)
}

return a.authServer.UnstableAssertSystemRole(ctx, req)
return a.authServer.AssertSystemRole(ctx, req)
}

// RegisterInventoryControlStream handles the upstream half of the control stream handshake, then passes the control stream to
Expand Down
Loading