mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 23:31:08 +08:00
944 lines
30 KiB
Go
944 lines
30 KiB
Go
// 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 aws
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
awssession "github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/aws/aws-sdk-go/service/kms"
|
|
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
|
"github.com/aws/aws-sdk-go/service/ssm"
|
|
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
|
|
"github.com/aws/aws-sdk-go/service/sts"
|
|
"github.com/aws/aws-sdk-go/service/sts/stsiface"
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/exp/slices"
|
|
|
|
awsutils "github.com/gravitational/teleport/api/utils/aws"
|
|
awslib "github.com/gravitational/teleport/lib/cloud/aws"
|
|
"github.com/gravitational/teleport/lib/config"
|
|
"github.com/gravitational/teleport/lib/configurators"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/srv/db/secrets"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
const (
|
|
// DefaultPolicyName default policy name.
|
|
DefaultPolicyName = "DatabaseAccess"
|
|
// databasePolicyDescription description used on the policy created.
|
|
databasePolicyDescription = "Used by Teleport database agents for discovering AWS-hosted databases."
|
|
// discoveryServicePolicyDescription description used on the policy created.
|
|
discoveryServicePolicyDescription = "Used by Teleport the discovery service to discover AWS-hosted services."
|
|
// boundarySuffix boundary policies will have this suffix.
|
|
boundarySuffix = "Boundary"
|
|
// policyTeleportTagKey key of the tag added to the policies created.
|
|
policyTeleportTagKey = "teleport"
|
|
// policyTeleportTagValue value of the tag added to the policies created.
|
|
policyTeleportTagValue = ""
|
|
// defaultAttachUser default user that the policy will be attached to.
|
|
defaultAttachUser = "username"
|
|
)
|
|
|
|
type databaseActions struct {
|
|
// discovery is a list of actions used for database discovery.
|
|
discovery []string
|
|
// iamAuth is a list of actions used for enabling IAM auth.
|
|
iamAuth []string
|
|
// metadata is a list of actions used for fetching database metadata
|
|
// (excluding the ones already in "discovery").
|
|
metadata []string
|
|
// managedUsers is a list of actions used for managing database users.
|
|
managedUsers []string
|
|
// boundary is a list of actions only used for boundary policies.
|
|
boundary []string
|
|
|
|
requireIAMEdit bool
|
|
requireSecretsManager bool
|
|
}
|
|
|
|
func (a databaseActions) buildStatementForDiscovery() *awslib.Statement {
|
|
// Note that currently extra boundary policies are not required for discovery service.
|
|
return &awslib.Statement{
|
|
Effect: awslib.EffectAllow,
|
|
Actions: a.discovery,
|
|
Resources: []string{"*"},
|
|
}
|
|
}
|
|
|
|
func (a databaseActions) buildStatement(boundary bool) *awslib.Statement {
|
|
var actions []string
|
|
actions = append(actions, a.discovery...)
|
|
actions = append(actions, a.iamAuth...)
|
|
actions = append(actions, a.metadata...)
|
|
actions = append(actions, a.managedUsers...)
|
|
if boundary {
|
|
actions = append(actions, a.boundary...)
|
|
}
|
|
return &awslib.Statement{
|
|
Effect: awslib.EffectAllow,
|
|
Actions: actions,
|
|
Resources: []string{"*"},
|
|
}
|
|
}
|
|
|
|
var (
|
|
// defaultPolicyTags default list of tags present at the managed policies.
|
|
defaultPolicyTags = map[string]string{policyTeleportTagKey: policyTeleportTagValue}
|
|
// userBaseActions list of actions used when target is an user.
|
|
userBaseActions = []string{"iam:GetUserPolicy", "iam:PutUserPolicy", "iam:DeleteUserPolicy"}
|
|
// roleBaseActions list of actions used when target is a role.
|
|
roleBaseActions = []string{"iam:GetRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy"}
|
|
// secretsManagerActions is a list of actions used for SecretsManager.
|
|
secretsManagerActions = []string{
|
|
"secretsmanager:DescribeSecret",
|
|
"secretsmanager:CreateSecret",
|
|
"secretsmanager:UpdateSecret",
|
|
"secretsmanager:DeleteSecret",
|
|
"secretsmanager:GetSecretValue",
|
|
"secretsmanager:PutSecretValue",
|
|
"secretsmanager:TagResource",
|
|
}
|
|
// kmsActions is a list of actions used for custom KMS keys.
|
|
kmsActions = []string{
|
|
"kms:GenerateDataKey",
|
|
"kms:Decrypt",
|
|
}
|
|
// ec2Actions is a list of actions used for EC2 auto-discovery
|
|
ec2Actions = []string{
|
|
"ec2:DescribeInstances",
|
|
"ssm:GetCommandInvocation",
|
|
"ssm:SendCommand",
|
|
}
|
|
// rdsActions contains IAM actions for services.AWSMatcherRDS (RDS
|
|
// instances and Aurora clusters).
|
|
rdsActions = databaseActions{
|
|
discovery: []string{"rds:DescribeDBInstances", "rds:DescribeDBClusters"},
|
|
iamAuth: []string{"rds:ModifyDBInstance", "rds:ModifyDBCluster"},
|
|
boundary: []string{"rds-db:connect"},
|
|
requireIAMEdit: true,
|
|
}
|
|
// rdsProxyActions contains IAM actions for services.AWSMatcherRDSProxy.
|
|
rdsProxyActions = databaseActions{
|
|
discovery: []string{
|
|
"rds:DescribeDBProxies",
|
|
"rds:DescribeDBProxyEndpoints",
|
|
"rds:DescribeDBProxyTargets",
|
|
"rds:ListTagsForResource",
|
|
},
|
|
boundary: []string{"rds-db:connect"},
|
|
requireIAMEdit: true,
|
|
}
|
|
// redshiftActions contains IAM actions for services.AWSMatcherRedshift.
|
|
redshiftActions = databaseActions{
|
|
discovery: []string{"redshift:DescribeClusters"},
|
|
boundary: []string{"redshift:GetClusterCredentials"},
|
|
requireIAMEdit: true,
|
|
}
|
|
// redshiftServerlessActions contains IAM actions for services.AWSMatcherRedshiftServerless.
|
|
redshiftServerlessActions = databaseActions{
|
|
discovery: []string{
|
|
"redshift-serverless:ListWorkgroups",
|
|
"redshift-serverless:ListEndpointAccess",
|
|
"redshift-serverless:ListTagsForResource",
|
|
},
|
|
metadata: []string{
|
|
"redshift-serverless:GetEndpointAccess",
|
|
"redshift-serverless:GetWorkgroup",
|
|
},
|
|
boundary: []string{"sts:AssumeRole"},
|
|
}
|
|
// elastiCacheActions contains IAM actions for services.AWSMatcherElastiCache.
|
|
elastiCacheActions = databaseActions{
|
|
discovery: []string{
|
|
"elasticache:ListTagsForResource",
|
|
"elasticache:DescribeReplicationGroups",
|
|
"elasticache:DescribeCacheClusters",
|
|
"elasticache:DescribeCacheSubnetGroups",
|
|
},
|
|
managedUsers: []string{
|
|
"elasticache:DescribeUsers",
|
|
"elasticache:ModifyUser",
|
|
},
|
|
requireSecretsManager: true,
|
|
}
|
|
// memoryDBActions contains IAM actions for services.AWSMatcherMemoryDB.
|
|
memoryDBActions = databaseActions{
|
|
discovery: []string{
|
|
"memorydb:ListTags",
|
|
"memorydb:DescribeClusters",
|
|
"memorydb:DescribeSubnetGroups",
|
|
},
|
|
managedUsers: []string{
|
|
"memorydb:DescribeUsers",
|
|
"memorydb:UpdateUser",
|
|
},
|
|
requireSecretsManager: true,
|
|
}
|
|
// awsKeyspacesActions contains IAM actions for static AWS Keyspaces databases.
|
|
awsKeyspacesActions = databaseActions{
|
|
boundary: []string{"sts:AssumeRole"},
|
|
}
|
|
// dynamodbActions contains IAM actions for static AWS DynamoDB databases.
|
|
dynamodbActions = databaseActions{
|
|
boundary: []string{"sts:AssumeRole"},
|
|
}
|
|
)
|
|
|
|
// awsConfigurator defines the AWS database configurator.
|
|
type awsConfigurator struct {
|
|
// config AWS configurator list of configs.
|
|
config ConfiguratorConfig
|
|
// actions list of the configurator actions, those are populated on the
|
|
// `build` function.
|
|
actions []configurators.ConfiguratorAction
|
|
}
|
|
|
|
type ConfiguratorConfig struct {
|
|
// Flags user-provided flags to configure/execute the configurator.
|
|
Flags configurators.BootstrapFlags
|
|
// FileConfig Teleport database agent config.
|
|
FileConfig *config.FileConfig
|
|
// AWSSession current AWS session.
|
|
AWSSession *awssession.Session
|
|
// AWSSTSClient AWS STS client.
|
|
AWSSTSClient stsiface.STSAPI
|
|
// AWSSSMClient AWS SSM Client
|
|
AWSSSMClient ssmiface.SSMAPI
|
|
// Policies instance of the `Policies` that the actions use.
|
|
Policies awslib.Policies
|
|
// Identity is the current AWS credentials chain identity.
|
|
Identity awslib.Identity
|
|
}
|
|
|
|
// CheckAndSetDefaults checks and set configuration default values.
|
|
func (c *ConfiguratorConfig) CheckAndSetDefaults() error {
|
|
if c.FileConfig == nil {
|
|
return trace.BadParameter("config file is required")
|
|
}
|
|
|
|
// When running the command in manual mode, we want to have zero dependency
|
|
// with AWS configurations (like awscli or environment variables), so that
|
|
// the user can run this command and generate the instructions without any
|
|
// pre-requisite.
|
|
if !c.Flags.Manual {
|
|
var err error
|
|
|
|
if c.AWSSession == nil {
|
|
c.AWSSession, err = awssession.NewSessionWithOptions(awssession.Options{
|
|
SharedConfigState: awssession.SharedConfigEnable,
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if c.Identity == nil {
|
|
if c.AWSSTSClient == nil {
|
|
c.AWSSTSClient = sts.New(c.AWSSession)
|
|
}
|
|
|
|
c.Identity, err = awslib.GetIdentityWithClient(context.Background(), c.AWSSTSClient)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if c.AWSSSMClient == nil {
|
|
c.AWSSSMClient = ssm.New(c.AWSSession)
|
|
}
|
|
|
|
if c.Policies == nil {
|
|
c.Policies = awslib.NewPolicies(c.Identity.GetPartition(), c.Identity.GetAccountID(), iam.New(c.AWSSession))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewAWSConfigurator creates an instance of awsConfigurator and builds its
|
|
// actions.
|
|
func NewAWSConfigurator(config ConfiguratorConfig) (configurators.Configurator, error) {
|
|
err := config.CheckAndSetDefaults()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
actions, err := buildActions(config)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &awsConfigurator{config, actions}, nil
|
|
}
|
|
|
|
// IsEmpty checks if the configurator has no actions.
|
|
func (a *awsConfigurator) IsEmpty() bool {
|
|
return len(a.actions) == 0
|
|
}
|
|
|
|
// Name returns human-readable configurator name.
|
|
func (a *awsConfigurator) Name() string {
|
|
return "AWS"
|
|
}
|
|
|
|
// Actions list of configurator actions.
|
|
func (a *awsConfigurator) Actions() []configurators.ConfiguratorAction {
|
|
return a.actions
|
|
}
|
|
|
|
// awsPolicyCreator creates a `PolicyDocument` on AWS, it also stores the policy
|
|
// ARN in the context.
|
|
type awsPolicyCreator struct {
|
|
// policies `Policies` used to upsert the policy document.
|
|
policies awslib.Policies
|
|
// isBoundary indicates if the policy created is a boundary or not.
|
|
isBoundary bool
|
|
// policy document that will be created on AWS.
|
|
policy *awslib.Policy
|
|
// formattedPolicy human-readable representation of the policy document.
|
|
formattedPolicy string
|
|
}
|
|
|
|
// Description returns what the action will perform.
|
|
func (a *awsPolicyCreator) Description() string {
|
|
return fmt.Sprintf("Create IAM Policy %q", a.policy.Name)
|
|
}
|
|
|
|
// Details returns the policy document that will be created.
|
|
func (a *awsPolicyCreator) Details() string {
|
|
return a.formattedPolicy
|
|
}
|
|
|
|
// Execute upserts the policy and store its ARN in the action context.
|
|
func (a *awsPolicyCreator) Execute(ctx context.Context, actionCtx *configurators.ConfiguratorActionContext) error {
|
|
if a.policies == nil {
|
|
return trace.BadParameter("policy helper not initialized")
|
|
}
|
|
arn, err := a.policies.Upsert(ctx, a.policy)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if a.isBoundary {
|
|
actionCtx.AWSPolicyBoundaryArn = arn
|
|
} else {
|
|
actionCtx.AWSPolicyArn = arn
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// awsPoliciesAttacher attach policy and policy boundary to a target. Both
|
|
// policies ARN are retrieved from the `Execute` `context.Context`.
|
|
type awsPoliciesAttacher struct {
|
|
// policies `Policies` used to attach policy and policy boundary.
|
|
policies awslib.Policies
|
|
// target identity where the policy will be attached to.
|
|
target awslib.Identity
|
|
}
|
|
|
|
// Description returns what the action will perform.
|
|
func (a *awsPoliciesAttacher) Description() string {
|
|
return fmt.Sprintf("Attach IAM policies to %q", a.target.GetName())
|
|
}
|
|
|
|
// Details attacher doesn't have any extra detail, this function returns an
|
|
// empty string.
|
|
func (a *awsPoliciesAttacher) Details() string {
|
|
return ""
|
|
}
|
|
|
|
// Execute retrieves policy and policy boundary ARNs from
|
|
// `ConfiguratorActionContext` and attach them to the `target`.
|
|
func (a *awsPoliciesAttacher) Execute(ctx context.Context, actionCtx *configurators.ConfiguratorActionContext) error {
|
|
if a.policies == nil {
|
|
return trace.BadParameter("policy helper not initialized")
|
|
}
|
|
|
|
if actionCtx.AWSPolicyArn == "" {
|
|
return trace.BadParameter("policy ARN not present")
|
|
}
|
|
|
|
if actionCtx.AWSPolicyBoundaryArn == "" {
|
|
return trace.BadParameter("policy boundary ARN not present")
|
|
}
|
|
|
|
err := a.policies.Attach(ctx, actionCtx.AWSPolicyArn, a.target)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
err = a.policies.AttachBoundary(ctx, actionCtx.AWSPolicyBoundaryArn, a.target)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildDiscoveryActions(config ConfiguratorConfig, target awslib.Identity) ([]configurators.ConfiguratorAction, error) {
|
|
actions, err := buildCommonActions(config, target)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
ssmDocumentCreators, err := buildSSMDocuments(config.AWSSSMClient, config.Flags, config.FileConfig)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
actions = append(actions, ssmDocumentCreators...)
|
|
return actions, nil
|
|
}
|
|
|
|
func buildCommonActions(config ConfiguratorConfig, target awslib.Identity) ([]configurators.ConfiguratorAction, error) {
|
|
// Generate policies.
|
|
policy, err := buildPolicyDocument(config.Flags, config.FileConfig, target, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
policyBoundary, err := buildPolicyDocument(config.Flags, config.FileConfig, target, true)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// If the policy has no statements means that the agent doesn't require
|
|
// any IAM permission. In this case, return without errors and with empty
|
|
// actions.
|
|
if len(policy.Document.Statements) == 0 {
|
|
return []configurators.ConfiguratorAction{}, nil
|
|
}
|
|
|
|
formattedPolicy, err := policy.Document.Marshal()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
var actions []configurators.ConfiguratorAction
|
|
|
|
// Create IAM Policy.
|
|
actions = append(actions, &awsPolicyCreator{
|
|
policies: config.Policies,
|
|
policy: policy,
|
|
formattedPolicy: formattedPolicy,
|
|
})
|
|
|
|
formattedPolicyBoundary, err := policyBoundary.Document.Marshal()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// Create IAM Policy boundary.
|
|
actions = append(actions, &awsPolicyCreator{
|
|
policies: config.Policies,
|
|
policy: policyBoundary,
|
|
formattedPolicy: formattedPolicyBoundary,
|
|
isBoundary: true,
|
|
})
|
|
|
|
// Attach both policies to the target.
|
|
actions = append(actions, &awsPoliciesAttacher{policies: config.Policies, target: target})
|
|
return actions, nil
|
|
}
|
|
|
|
// buildActions generates the policy documents and configurator actions.
|
|
func buildActions(config ConfiguratorConfig) ([]configurators.ConfiguratorAction, error) {
|
|
// Identity is going to be empty (`nil`) when running the command on
|
|
// `Manual` mode, place a wildcard to keep the generated policies valid.
|
|
accountID := "*"
|
|
partitionID := "*"
|
|
if config.Identity != nil {
|
|
accountID = config.Identity.GetAccountID()
|
|
partitionID = config.Identity.GetPartition()
|
|
}
|
|
|
|
// Define the target and target type.
|
|
target, err := policiesTarget(config.Flags, accountID, partitionID, config.Identity)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if config.Flags.DiscoveryService {
|
|
return buildDiscoveryActions(config, target)
|
|
}
|
|
return buildCommonActions(config, target)
|
|
}
|
|
|
|
// policiesTarget defines which target and its type the policies will be
|
|
// attached to.
|
|
func policiesTarget(flags configurators.BootstrapFlags, accountID string, partitionID string, identity awslib.Identity) (awslib.Identity, error) {
|
|
if flags.AttachToUser != "" {
|
|
userArn := flags.AttachToUser
|
|
if !arn.IsARN(flags.AttachToUser) {
|
|
userArn = fmt.Sprintf("arn:%s:iam::%s:user/%s", partitionID, accountID, flags.AttachToUser)
|
|
}
|
|
|
|
return awslib.IdentityFromArn(userArn)
|
|
}
|
|
|
|
if flags.AttachToRole != "" {
|
|
roleArn := flags.AttachToRole
|
|
if !arn.IsARN(flags.AttachToRole) {
|
|
roleArn = fmt.Sprintf("arn:%s:iam::%s:role/%s", partitionID, accountID, flags.AttachToRole)
|
|
}
|
|
|
|
return awslib.IdentityFromArn(roleArn)
|
|
}
|
|
|
|
if identity == nil {
|
|
return awslib.IdentityFromArn(fmt.Sprintf("arn:%s:iam::%s:user/%s", partitionID, accountID, defaultAttachUser))
|
|
}
|
|
|
|
return identity, nil
|
|
}
|
|
|
|
// buildPolicyDocument builds the policy document.
|
|
func buildPolicyDocument(flags configurators.BootstrapFlags, fileConfig *config.FileConfig, target awslib.Identity, boundary bool) (*awslib.Policy, error) {
|
|
policyDoc := awslib.NewPolicyDocument()
|
|
policyDescription := databasePolicyDescription
|
|
policyName := flags.PolicyName
|
|
|
|
if boundary {
|
|
policyName += boundarySuffix
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
policyDescription = discoveryServicePolicyDescription
|
|
|
|
if isEC2AutoDiscoveryEnabled(flags, fileConfig) {
|
|
policyDoc.EnsureStatements(buildEC2AutoDiscoveryStatements()...)
|
|
}
|
|
}
|
|
|
|
// Build statements for databases.
|
|
var requireSecretsManager, requireIAMEdit bool
|
|
var allActions []databaseActions
|
|
if hasRDSDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, rdsActions)
|
|
}
|
|
if hasRDSProxyDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, rdsProxyActions)
|
|
}
|
|
if hasRedshiftDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, redshiftActions)
|
|
}
|
|
if hasRedshiftServerlessDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, redshiftServerlessActions)
|
|
}
|
|
if hasElastiCacheDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, elastiCacheActions)
|
|
}
|
|
if hasMemoryDBDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, memoryDBActions)
|
|
}
|
|
if hasAWSKeyspacesDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, awsKeyspacesActions)
|
|
}
|
|
if hasDynamoDBDatabases(flags, fileConfig) {
|
|
allActions = append(allActions, dynamodbActions)
|
|
}
|
|
|
|
for _, dbActions := range allActions {
|
|
if flags.DiscoveryService {
|
|
policyDoc.EnsureStatements(dbActions.buildStatementForDiscovery())
|
|
} else {
|
|
policyDoc.EnsureStatements(dbActions.buildStatement(boundary))
|
|
|
|
// Skip these for discovery service.
|
|
requireSecretsManager = requireSecretsManager || dbActions.requireSecretsManager
|
|
requireIAMEdit = requireIAMEdit || dbActions.requireIAMEdit
|
|
}
|
|
}
|
|
|
|
// For databases that need to access SecretsManager (and KMS).
|
|
if requireSecretsManager {
|
|
policyDoc.EnsureStatements(buildSecretsManagerStatements(fileConfig, target)...)
|
|
}
|
|
// For databases that need to edit IAM user/role policy.
|
|
if requireIAMEdit {
|
|
targetStatements, err := buildIAMEditStatements(target)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
policyDoc.EnsureStatements(targetStatements...)
|
|
}
|
|
|
|
return awslib.NewPolicy(
|
|
policyName,
|
|
policyDescription,
|
|
defaultPolicyTags,
|
|
policyDoc,
|
|
), nil
|
|
}
|
|
|
|
func getProxyAddrFromFileConfig(fc *config.FileConfig) (string, error) {
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.PublicAddr, defaults.HTTPListenPort)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(addrs) == 0 {
|
|
return fmt.Sprintf("https://<proxy address>:%d", defaults.HTTPListenPort), nil
|
|
}
|
|
addr := addrs[0]
|
|
|
|
return fmt.Sprintf("https://%s", addr.String()), nil
|
|
}
|
|
|
|
func buildSSMDocuments(ssm ssmiface.SSMAPI, flags configurators.BootstrapFlags, fileConfig *config.FileConfig) ([]configurators.ConfiguratorAction, error) {
|
|
var creators []configurators.ConfiguratorAction
|
|
proxyAddr, err := getProxyAddrFromFileConfig(fileConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, matcher := range fileConfig.Discovery.AWSMatchers {
|
|
if !slices.Contains(matcher.Types, services.AWSMatcherEC2) {
|
|
continue
|
|
}
|
|
ssmCreator := awsSSMDocumentCreator{
|
|
ssm: ssm,
|
|
Name: matcher.SSM.DocumentName,
|
|
Contents: EC2DiscoverySSMDocument(proxyAddr),
|
|
}
|
|
creators = append(creators, &ssmCreator)
|
|
}
|
|
return creators, nil
|
|
}
|
|
|
|
func isEC2AutoDiscoveryEnabled(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceEC2Permissions {
|
|
return true
|
|
}
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherEC2, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
// hasRDSDatabases checks if the agent needs permission for
|
|
// RDS/Aurora databases.
|
|
func hasRDSDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceRDSPermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRDS, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
// isRDSAutoDiscoveryEnabled checks if the agent needs permission for
|
|
// RDS/Aurora auto-discovery.
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRDS, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, isRDSEndpoint)
|
|
}
|
|
|
|
// hasRDSProxyDatabases checks if the agent needs permission for
|
|
// RDS Proxy databases.
|
|
func hasRDSProxyDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceRDSProxyPermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRDSProxy, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRDSProxy, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, isRDSProxyEndpoint)
|
|
}
|
|
|
|
// hasRedshiftDatabases checks if the agent needs permission for
|
|
// Redshift databases.
|
|
func hasRedshiftDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceRedshiftPermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRedshift, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRedshift, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, awsutils.IsRedshiftEndpoint)
|
|
}
|
|
|
|
// hasRedshiftServerlessDatabases checks if the agent needs permission for
|
|
// Redshift Serverless databases.
|
|
func hasRedshiftServerlessDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceRedshiftServerlessPermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRedshiftServerless, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherRedshiftServerless, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, awsutils.IsRedshiftServerlessEndpoint)
|
|
}
|
|
|
|
// hasElastiCacheDatabases checks if the agent needs permission for
|
|
// ElastiCache databases.
|
|
func hasElastiCacheDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceElastiCachePermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherElastiCache, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherElastiCache, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, awsutils.IsElastiCacheEndpoint)
|
|
}
|
|
|
|
// hasMemoryDBDatabases checks if the agent needs permission for
|
|
// ElastiCache databases.
|
|
func hasMemoryDBDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
if flags.ForceMemoryDBPermissions {
|
|
return true
|
|
}
|
|
|
|
if flags.DiscoveryService {
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherMemoryDB, fileConfig.Discovery.AWSMatchers)
|
|
}
|
|
|
|
return isAutoDiscoveryEnabledForMatcher(services.AWSMatcherMemoryDB, fileConfig.Databases.AWSMatchers) ||
|
|
findEndpointIs(fileConfig, awsutils.IsMemoryDBEndpoint)
|
|
}
|
|
|
|
// hasAWSKeyspacesDatabases checks if the agent needs permission for AWS Keyspaces.
|
|
func hasAWSKeyspacesDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
// There is no auto discovery for AWS Keyspaces.
|
|
if flags.DiscoveryService {
|
|
return false
|
|
}
|
|
|
|
return findDatabaseIs(fileConfig, func(database *config.Database) bool {
|
|
return database.Protocol == defaults.ProtocolCassandra && database.AWS.AccountID != ""
|
|
})
|
|
}
|
|
|
|
// hasDynamoDBDatabases checks if the agent needs permission for AWS DynamoDB.
|
|
func hasDynamoDBDatabases(flags configurators.BootstrapFlags, fileConfig *config.FileConfig) bool {
|
|
// There is no auto discovery for AWS DynamoDB.
|
|
if flags.DiscoveryService {
|
|
return false
|
|
}
|
|
|
|
return findDatabaseIs(fileConfig, func(database *config.Database) bool {
|
|
return database.Protocol == defaults.ProtocolDynamoDB
|
|
})
|
|
}
|
|
|
|
// isAutoDiscoveryEnabledForMatcher returns true if provided AWS matcher type
|
|
// is found.
|
|
func isAutoDiscoveryEnabledForMatcher(matcherType string, matchers []config.AWSMatcher) bool {
|
|
for _, matcher := range matchers {
|
|
for _, databaseType := range matcher.Types {
|
|
if databaseType == matcherType {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// findEndpointIs returns true if provided check returns true for any static
|
|
// endpoint.
|
|
func findEndpointIs(fileConfig *config.FileConfig, endpointIs func(string) bool) bool {
|
|
return findDatabaseIs(fileConfig, func(database *config.Database) bool {
|
|
return endpointIs(database.URI)
|
|
})
|
|
}
|
|
|
|
// findDatabaseIs returns true if provided check returns true for any static
|
|
// database config.
|
|
func findDatabaseIs(fileConfig *config.FileConfig, is func(*config.Database) bool) bool {
|
|
for _, database := range fileConfig.Databases.Databases {
|
|
if is(database) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isRDSEndpoint returns true if the endpoint is an endpoint for RDS instance or Aurora cluster.
|
|
func isRDSEndpoint(uri string) bool {
|
|
details, err := awsutils.ParseRDSEndpoint(uri)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return !details.IsProxy()
|
|
}
|
|
|
|
// isRDSProxyEndpoint returns true if the endpoint is an endpoint for RDS Proxy.
|
|
func isRDSProxyEndpoint(uri string) bool {
|
|
details, err := awsutils.ParseRDSEndpoint(uri)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return details.IsProxy()
|
|
}
|
|
|
|
// buildIAMEditStatements returns IAM statements necessary for the Teleport
|
|
// agent to edit user/role permissions.
|
|
func buildIAMEditStatements(target awslib.Identity) ([]*awslib.Statement, error) {
|
|
statement := &awslib.Statement{
|
|
Effect: awslib.EffectAllow,
|
|
Resources: []string{target.String()},
|
|
}
|
|
|
|
switch target.(type) {
|
|
case awslib.User, *awslib.User:
|
|
statement.Actions = userBaseActions
|
|
case awslib.Role, *awslib.Role:
|
|
statement.Actions = roleBaseActions
|
|
default:
|
|
return nil, trace.BadParameter("policies target must be an user or role, received %T", target)
|
|
}
|
|
|
|
return []*awslib.Statement{statement}, nil
|
|
}
|
|
|
|
// buildEC2AutoDiscoveryStatements returns IAM statements necessary for
|
|
// EC2 instance auto-discovery.
|
|
func buildEC2AutoDiscoveryStatements() []*awslib.Statement {
|
|
return []*awslib.Statement{
|
|
{
|
|
Effect: awslib.EffectAllow,
|
|
Actions: ec2Actions,
|
|
Resources: []string{"*"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildSecretsManagerStatements returns IAM statements necessary for using AWS
|
|
// Secrets Manager.
|
|
func buildSecretsManagerStatements(fileConfig *config.FileConfig, target awslib.Identity) []*awslib.Statement {
|
|
// Populate resources with the default secrets prefix.
|
|
secretsManagerStatement := &awslib.Statement{
|
|
Effect: awslib.EffectAllow,
|
|
Actions: secretsManagerActions,
|
|
Resources: []string{buildSecretsManagerARN(target, secrets.DefaultKeyPrefix)},
|
|
}
|
|
// KMS statement is only required when using custom KMS keys.
|
|
kmsStatement := &awslib.Statement{
|
|
Effect: awslib.EffectAllow,
|
|
Actions: kmsActions,
|
|
}
|
|
|
|
addedSecretPrefixes := map[string]bool{}
|
|
addedKMSKeyIDs := map[string]bool{}
|
|
for _, database := range fileConfig.Databases.Databases {
|
|
if !awsutils.IsElastiCacheEndpoint(database.URI) &&
|
|
!awsutils.IsMemoryDBEndpoint(database.URI) {
|
|
continue
|
|
}
|
|
|
|
// Build Secrets Manager resources.
|
|
prefix := database.AWS.SecretStore.KeyPrefix
|
|
if prefix != "" && !addedSecretPrefixes[prefix] {
|
|
addedSecretPrefixes[prefix] = true
|
|
secretsManagerStatement.Resources = append(
|
|
secretsManagerStatement.Resources,
|
|
buildSecretsManagerARN(target, prefix),
|
|
)
|
|
}
|
|
|
|
// Build KMS resources.
|
|
kmsKeyID := database.AWS.SecretStore.KMSKeyID
|
|
if kmsKeyID != "" && !addedKMSKeyIDs[kmsKeyID] {
|
|
addedKMSKeyIDs[kmsKeyID] = true
|
|
kmsStatement.Resources = append(
|
|
kmsStatement.Resources,
|
|
buildARN(target, kms.ServiceName, "key/"+kmsKeyID),
|
|
)
|
|
}
|
|
}
|
|
|
|
statements := []*awslib.Statement{
|
|
secretsManagerStatement,
|
|
}
|
|
if len(kmsStatement.Resources) > 0 {
|
|
statements = append(statements, kmsStatement)
|
|
}
|
|
return statements
|
|
}
|
|
|
|
// buildSecretsManagerARN builds an ARN of a secret used for Secrets Manager
|
|
// IAM policies.
|
|
func buildSecretsManagerARN(target awslib.Identity, keyPrefix string) string {
|
|
return buildARN(
|
|
target,
|
|
secretsmanager.ServiceName,
|
|
fmt.Sprintf("secret:%s/*", strings.TrimSuffix(keyPrefix, "/")),
|
|
)
|
|
}
|
|
|
|
// buildARN builds an ARN string with provided identity, service and resource.
|
|
func buildARN(target awslib.Identity, service, resource string) string {
|
|
arn := arn.ARN{
|
|
Partition: target.GetPartition(),
|
|
AccountID: target.GetAccountID(),
|
|
Region: "*",
|
|
Service: service,
|
|
Resource: resource,
|
|
}
|
|
return arn.String()
|
|
}
|
|
|
|
type awsSSMDocumentCreator struct {
|
|
Contents string
|
|
ssm ssmiface.SSMAPI
|
|
Name string
|
|
}
|
|
|
|
// Description returns what the action will perform.
|
|
func (a *awsSSMDocumentCreator) Description() string {
|
|
return fmt.Sprintf("Create SSM Document %q", a.Name)
|
|
}
|
|
|
|
// Details returns the policy document that will be created.
|
|
func (a *awsSSMDocumentCreator) Details() string {
|
|
return a.Contents
|
|
}
|
|
|
|
// Execute upserts the policy and store its ARN in the action context.
|
|
func (a *awsSSMDocumentCreator) Execute(ctx context.Context, actionCtx *configurators.ConfiguratorActionContext) error {
|
|
_, err := a.ssm.CreateDocumentWithContext(ctx, &ssm.CreateDocumentInput{
|
|
Content: aws.String(a.Contents),
|
|
Name: aws.String(a.Name),
|
|
DocumentType: aws.String(ssm.DocumentTypeCommand),
|
|
DocumentFormat: aws.String("YAML"),
|
|
})
|
|
|
|
if err != nil {
|
|
var aErr awserr.Error
|
|
if errors.As(err, &aErr) && aErr.Code() == ssm.ErrCodeDocumentAlreadyExists {
|
|
fmt.Printf("⚠️ Warning: SSM document %s already exists. Not overwriting.\n", a.Name)
|
|
return nil
|
|
}
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|