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 @@ -1070,6 +1070,15 @@ func (c *Client) GenerateOpenSSHCert(ctx context.Context, req *proto.OpenSSHCert
return cert, nil
}

// 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,601 changes: 1,647 additions & 954 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

47 changes: 46 additions & 1 deletion api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ message Watch {
message HostCertsRequest {
reserved 12; // system_role_assertion_id
reserved "UnstableSystemRoleAssertionID";

// HostID is a unique ID of the host.
string HostID = 1 [(gogoproto.jsontag) = "host_id"];
// NodeName is a user-friendly host name.
Expand Down Expand Up @@ -87,6 +86,11 @@ message HostCertsRequest {
(gogoproto.jsontag) = "system_roles,omitempty",
(gogoproto.casttype) = "github.com/gravitational/teleport/api/types.SystemRole"
];
// 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 @@ -2188,6 +2192,41 @@ message GetSSODiagnosticInfoRequest {
string AuthRequestID = 2 [(gogoproto.jsontag) = "auth_request_id"];
}

// 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"
];
}

// 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"
Comment thread
fspmarshall marked this conversation as resolved.
Outdated
];
}

// UpstreamInventoryOneOf is the upstream message for the inventory control stream,
// sent from teleport instances to the auth server.
message UpstreamInventoryOneOf {
Expand Down Expand Up @@ -3293,6 +3332,12 @@ service AuthService {
// all be appended.
rpc GetClusterCACert(google.protobuf.Empty) returns (GetClusterCACertResponse);

// 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
238 changes: 238 additions & 0 deletions integration/instance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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/cloud/imds"
"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
}
Comment on lines 80 to 88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any guarantees that returned listen address will still be free after the listener is closed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ish... binding on 0 and then immediately closing is a well documented pattern for finding free ports on *nix systems during tests, and from my research seems to be accounted for/expected by socket API implementers who try to ensure that the logic behind selecting a free port is either random or biased toward not rapidly reusing free ports. For example, on macOS binding 0 doesn't return a duplicate until it has cycled through returning all other available free ports first.


// 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()
authCfg.InstanceMetadataClient = imds.NewDisabledIMDSClient()

authRunErrCh := make(chan error, 1)
authIdentitiesCh := make(chan *auth.Identity, 2)
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)
}

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

select {
case authIdentitiesCh <- identity:
default:
}

return proc, nil
})
}()

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

select {
case <-timeout:
t.Fatal("timed out waiting for second auth identity")
case identity := <-authIdentitiesCh:
require.ElementsMatch(t, []string{string(types.RoleAuth), string(types.RoleProxy), string(types.RoleNode)}, identity.SystemRoles)
}

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
agentCfg.InstanceMetadataClient = imds.NewDisabledIMDSClient()

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 @@
4b60753c-a4c4-435a-8045-b77faabb41d7
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 @@
1feff6fc-865b-47c1-8a5d-f10e1811c30e
Binary file added integration/testdata/auth/proc/sqlite.db
Binary file not shown.
16 changes: 16 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4400,6 +4400,22 @@ func (a *Server) GenerateHostCerts(ctx context.Context, req *proto.HostCertsRequ
}, nil
}

// 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))
}

// 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)
}

func (a *Server) RegisterInventoryControlStream(ics client.UpstreamInventoryControlStream, hello proto.UpstreamInventoryHello) error {
// upstream hello is pulled and checked at rbac layer. we wait to send the downstream hello until we get here
// in order to simplify creation of in-memory streams when dealing with local auth (note: in theory we could
Expand Down
Loading
Loading