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
24 changes: 24 additions & 0 deletions lib/cloud/aws/policy_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,27 @@ func StatementForRDSDBConnect() *Statement {
Resources: allResources,
}
}

// StatementForEC2InstanceConnectEndpoint returns the statement that allows the flow for accessing
// an EC2 instance using its private IP, using EC2 Instance Connect Endpoint.
func StatementForEC2InstanceConnectEndpoint() *Statement {
return &Statement{
Effect: EffectAllow,
Actions: []string{
"ec2:DescribeInstances",
"ec2:DescribeInstanceConnectEndpoints",
"ec2:DescribeSecurityGroups",

// Create ICE requires the following actions:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/permissions-for-ec2-instance-connect-endpoint.html
"ec2:CreateInstanceConnectEndpoint",
"ec2:CreateTags",
"ec2:CreateNetworkInterface",
"iam:CreateServiceLinkedRole",

"ec2-instance-connect:SendSSHPublicKey",
"ec2-instance-connect:OpenTunnel",
},
Resources: allResources,
}
}
13 changes: 13 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ type CommandLineFlags struct {
// IntegrationConfDeployServiceIAMArguments contains the arguments of
// `teleport integration configure deployservice-iam` command
IntegrationConfDeployServiceIAMArguments IntegrationConfDeployServiceIAM

// IntegrationConfEICEIAMArguments contains the arguments of
// `teleport integration configure eice-iam` command
IntegrationConfEICEIAMArguments IntegrationConfEICEIAM
}

// IntegrationConfDeployServiceIAM contains the arguments of
Expand All @@ -215,6 +219,15 @@ type IntegrationConfDeployServiceIAM struct {
TaskRole string
}

// IntegrationConfEICEIAM contains the arguments of
// `teleport integration configure eice-iam` command
type IntegrationConfEICEIAM struct {
// Region is the AWS Region used to set up the client.
Region string
// Role is the AWS Role associated with the Integration
Role string
}

// ReadConfigFile reads /etc/teleport.yaml (or whatever is passed via --config flag)
// and overrides values in 'cfg' structure
func ReadConfigFile(cliConfigPath string) (*FileConfig, error) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/deployservice_iam_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func addPolicyToIntegrationRole(ctx context.Context, clt DeployServiceIAMConfigu
})
if err != nil {
if trace.IsNotFound(awslib.ConvertIAMv2Error(err)) {
return trace.NotFound("Role %q not found.", req.IntegrationRole)
return trace.NotFound("role %q not found.", req.IntegrationRole)
}
return trace.Wrap(err)
}
Expand Down
145 changes: 145 additions & 0 deletions lib/integrations/awsoidc/eice_iam_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
Copyright 2023 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 awsoidc

import (
"context"
"log"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/gravitational/trace"

awslib "github.com/gravitational/teleport/lib/cloud/aws"
)

const (
// defaultPolicyNameForEICE is the default name for the Inline Policy added to the IntegrationRole.
defaultPolicyNameForEICE = "EC2InstanceConnectEndpoint"
)

// EICEIAMConfigureRequest is a request to configure the required Policies to use the EC2 Instance Connect Endpoint feature.
type EICEIAMConfigureRequest struct {
// Region is the AWS Region.
// Used to set up the AWS SDK Client.
Region string

// IntegrationRole is the Integration's AWS Role used to set up Teleport as an OIDC IdP.
IntegrationRole string

// IntegrationRoleEICEPolicy is the Policy Name that is created to allow access to call AWS APIs.
// Defaults to EC2InstanceConnectEndpoint
IntegrationRoleEICEPolicy string
}

// CheckAndSetDefaults ensures the required fields are present.
func (r *EICEIAMConfigureRequest) CheckAndSetDefaults() error {
if r.Region == "" {
return trace.BadParameter("region is required")
}

if r.IntegrationRole == "" {
return trace.BadParameter("integration role is required")
}

if r.IntegrationRoleEICEPolicy == "" {
r.IntegrationRoleEICEPolicy = defaultPolicyNameForEICE
}

return nil
}

// EICEIAMConfigureClient describes the required methods to create the IAM Policies required for accessing EC2 instances usine EICE.
type EICEIAMConfigureClient interface {
// PutRolePolicy creates or replaces a Policy by its name in a IAM Role.
PutRolePolicy(ctx context.Context, params *iam.PutRolePolicyInput, optFns ...func(*iam.Options)) (*iam.PutRolePolicyOutput, error)
}

type defaultEICEIAMConfigureClient struct {
*iam.Client
}

// NewEICEIAMConfigureClient creates a new EICEIAMConfigureClient.
func NewEICEIAMConfigureClient(ctx context.Context, region string) (EICEIAMConfigureClient, error) {
if region == "" {
return nil, trace.BadParameter("region is required")
}

cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return nil, trace.Wrap(err)
}

return &defaultEICEIAMConfigureClient{
Client: iam.NewFromConfig(cfg),
}, nil
}

// ConfigureEICEIAM set ups the roles required for accessing an EC2 Instance using EICE.
// It creates an embedded policy with the following permissions:
//
// Action: List EC2 instances to add them as Teleport Nodes
// - ec2:DescribeInstances
//
// Action: List EC2 Instance Connect Endpoints so that knows if they must create one Endpoint.
// - ec2:DescribeInstanceConnectEndpoints
//
// Action: Select one or more SecurityGroups to apply to the EC2 Instance Connect Endpoints (the VPC's default SG is applied if no SG is provided).
// - ec2:DescribeSecurityGroups
//
// Action: Create EC2 Instance Connect Endpoint so the user can open a tunnel to the EC2 instance.
// More info: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/permissions-for-ec2-instance-connect-endpoint.html
// - ec2:CreateInstanceConnectEndpoint
// - ec2:CreateTags
// - ec2:CreateNetworkInterface
// - iam:CreateServiceLinkedRole
//
// Action: Send a temporary SSH Key to the target host.
// - ec2-instance-connect:SendSSHPublicKey
//
// Action: Open a Tunnel to the EC2 using the Endpoint
// - ec2-instance-connect:OpenTunnel
//
// The following actions must be allowed by the IAM Role assigned in the Client.
// - iam:PutRolePolicy
func ConfigureEICEIAM(ctx context.Context, clt EICEIAMConfigureClient, req EICEIAMConfigureRequest) error {
if err := req.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

ec2ICEPolicyDocument, err := awslib.NewPolicyDocument(
awslib.StatementForEC2InstanceConnectEndpoint(),
).Marshal()
if err != nil {
return trace.Wrap(err)
}

_, err = clt.PutRolePolicy(ctx, &iam.PutRolePolicyInput{
PolicyName: &req.IntegrationRoleEICEPolicy,
RoleName: &req.IntegrationRole,
PolicyDocument: &ec2ICEPolicyDocument,
})
if err != nil {
if trace.IsNotFound(awslib.ConvertIAMv2Error(err)) {
return trace.NotFound("role %q not found.", req.IntegrationRole)
}
return trace.Wrap(err)
}

log.Printf("IntegrationRole: IAM Policy %q added to Role %q\n", req.IntegrationRoleEICEPolicy, req.IntegrationRole)
return nil
}
138 changes: 138 additions & 0 deletions lib/integrations/awsoidc/eice_iam_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Copyright 2023 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 awsoidc

import (
"context"
"fmt"
"testing"

"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
)

func TestEICEIAMConfigReqDefaults(t *testing.T) {
baseReq := func() EICEIAMConfigureRequest {
return EICEIAMConfigureRequest{
Region: "us-east-1",
IntegrationRole: "integrationrole",
}
}

for _, tt := range []struct {
name string
req func() EICEIAMConfigureRequest
errCheck require.ErrorAssertionFunc
expected EICEIAMConfigureRequest
}{
{
name: "set defaults",
req: baseReq,
errCheck: require.NoError,
expected: EICEIAMConfigureRequest{
Region: "us-east-1",
IntegrationRole: "integrationrole",
IntegrationRoleEICEPolicy: "EC2InstanceConnectEndpoint",
},
},
{
name: "missing region",
req: func() EICEIAMConfigureRequest {
req := baseReq()
req.Region = ""
return req
},
errCheck: badParameterCheck,
},
{
name: "missing integration role",
req: func() EICEIAMConfigureRequest {
req := baseReq()
req.IntegrationRole = ""
return req
},
errCheck: badParameterCheck,
},
} {
t.Run(tt.name, func(t *testing.T) {
req := tt.req()
err := req.CheckAndSetDefaults()
tt.errCheck(t, err)
if err != nil {
return
}

require.Equal(t, tt.expected, req)
})
}
}

func TestEICEIAMConfig(t *testing.T) {
ctx := context.Background()
baseReq := func() EICEIAMConfigureRequest {
return EICEIAMConfigureRequest{
Region: "us-east-1",
IntegrationRole: "integrationrole",
}
}

for _, tt := range []struct {
name string
mockExistingRoles []string
req func() EICEIAMConfigureRequest
errCheck require.ErrorAssertionFunc
}{
{
name: "valid",
req: baseReq,
mockExistingRoles: []string{"integrationrole"},
errCheck: require.NoError,
},
{
name: "integration role does not exist",
mockExistingRoles: []string{},
req: baseReq,
errCheck: notFounCheck,
},
} {
t.Run(tt.name, func(t *testing.T) {
clt := mockEICEIAMConfigClient{
existingRoles: tt.mockExistingRoles,
}

err := ConfigureEICEIAM(ctx, &clt, tt.req())
tt.errCheck(t, err)
})
}
}

type mockEICEIAMConfigClient struct {
existingRoles []string
}

// PutRolePolicy creates or replaces a Policy by its name in a IAM Role.
func (m *mockEICEIAMConfigClient) PutRolePolicy(ctx context.Context, params *iam.PutRolePolicyInput, optFns ...func(*iam.Options)) (*iam.PutRolePolicyOutput, error) {
noSuchEntityMessage := fmt.Sprintf("role %q does not exist.", *params.RoleName)
if !slices.Contains(m.existingRoles, *params.RoleName) {
return nil, &iamTypes.NoSuchEntityException{
Message: &noSuchEntityMessage,
}
}
return nil, nil
}
1 change: 1 addition & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/scripts/integrations/configure/deployservice-iam.sh", h.WithLimiter(h.awsOIDCConfigureDeployServiceIAM))
h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/ec2", h.WithClusterAuth(h.awsOIDCListEC2))
h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/ec2ice", h.WithClusterAuth(h.awsOIDCListEC2ICE))
h.GET("/webapi/scripts/integrations/configure/eice-iam.sh", h.WithLimiter(h.awsOIDCConfigureEICEIAM))

// AWS OIDC Integration specific endpoints:
// Unauthenticated access to OpenID Configuration - used for AWS OIDC IdP integration
Expand Down
36 changes: 36 additions & 0 deletions lib/web/integrations_awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,42 @@ func (h *Handler) awsOIDCConfigureDeployServiceIAM(w http.ResponseWriter, r *htt
return nil, trace.Wrap(err)
}

// awsOIDCConfigureEICEIAM returns a script that configures the required IAM permissions to enable the usage of EC2 Instance Connect Endpoint
// to access EC2 instances.
func (h *Handler) awsOIDCConfigureEICEIAM(w http.ResponseWriter, r *http.Request, p httprouter.Params) (any, error) {
queryParams := r.URL.Query()

awsRegion := queryParams.Get("awsRegion")
if err := aws.IsValidRegion(awsRegion); err != nil {
return nil, trace.BadParameter("invalid awsRegion")
}

role := queryParams.Get("role")
if err := aws.IsValidIAMRoleName(role); err != nil {
return nil, trace.BadParameter("invalid role")
}

// The script must execute the following command:
// teleport integration configure eice-iam
argsList := []string{
"integration", "configure", "eice-iam",
fmt.Sprintf("--aws-region=%s", awsRegion),
fmt.Sprintf("--role=%s", role),
}
script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{
TeleportArgs: strings.Join(argsList, " "),
SuccessMessage: "Success! You can now go back to the browser to complete the EC2 enrollment.",
})
if err != nil {
return nil, trace.Wrap(err)
}

httplib.SetScriptHeaders(w.Header())
fmt.Fprint(w, script)

return nil, trace.Wrap(err)
}

// awsOIDCListEC2 returns a list of EC2 Instances using the ListEC2 action of the AWS OIDC Integration.
func (h *Handler) awsOIDCListEC2(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (any, error) {
ctx := r.Context()
Expand Down
Loading