aws-sc-cross-account-provision-enters-account

A principal in account B provisioning a product from a portfolio shared by account A (servicecatalog:ProvisionProduct) causes resources to be deployed in account A as the account-A launch role. The receiving-account principal effectively gains a foothold in account A via the launch role's permissions.

derived aws emits CanEnterAccount

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'portfolio_share': None} CrossAccountTrust {'target_account': None} {'principal': None} CanTrigger {'product': None} {'product': None} ExecutesAs {'launch_role': None}

where

node_type(?portfolio_share) == ResourcePolicy # AWS::ServiceCatalog::PortfolioShare node_type(?target_account) == Account # the sharing (source) account — account A ?portfolio_share represents the share of ?portfolio to ?target_account ?product is in ?portfolio ?principal.account != ?target_account # principal is in a different account (account B) ?launch_role.account == ?target_account # launch role resides in account A ?principal has EFFECTIVE servicecatalog:ProvisionProduct OR servicecatalog:UpdateProvisionedProduct on ?product via the imported portfolio ?product has a LAUNCH (not LocalRoleName) constraint, i.e., ConstraintType == LAUNCH and ConstraintParameters.RoleArn is in ?target_account

emit

source typeIdentity
target typeAccount
source?principal
target?target_account
permissionsservicecatalog:ProvisionProduct servicecatalog:UpdateProvisionedProduct
conditionstrust_relationship iam_permission
state logicACTIVE when (a) the CrossAccountTrust / PortfolioShare between ?portfolio_share and ?target_account is confirmed (DescribePortfolioShares shows the share AND the receiving account has accepted it via ImportPortfolio), (b) ?principal has EFFECTIVE servicecatalog:ProvisionProduct or UpdateProvisionedProduct on ?product in the imported portfolio, AND (c) ?product has a LaunchRole constraint with a role in ?target_account (not a LocalRoleName which resolves in the receiving account). CONDITIONAL(trust_relationship) when the portfolio share has been created but the receiving account has not yet accepted/imported it (ImportPortfolio not called). CONDITIONAL(parameter_controlled_resource) when the product template has no attacker-influenceable execution path (same gate as rule 1). BLOCKED when an SCP in either account denies the relevant servicecatalog or CFN actions; or when no LaunchRole constraint is present or when the constraint is LocalRoleName (role in receiving account, not cross-account).

Narrative

{principal.name} (account B) can provision product {product.name} from a portfolio shared by account {target_account.name} (account A, via {portfolio_share.name}). Service Catalog assumes the launch role {launch_role.name} (residing in account A) to deploy the product's CloudFormation resources there. {principal.name} gains a foothold in account A through the launch role's permissions, without ever holding sts:AssumeRole on {launch_role.name}.

Raw rule rules/derived/aws/servicecatalog.yaml

id: aws-sc-cross-account-provision-enters-account
emits: CanEnterAccount
description: A principal in account B provisioning a product from a portfolio shared by account A (servicecatalog:ProvisionProduct)
  causes resources to be deployed in account A as the account-A launch role. The receiving-account principal
  effectively gains a foothold in account A via the launch role's permissions.
match:
- - portfolio_share: null
  - CrossAccountTrust
  - target_account: null
- - principal: null
  - CanTrigger
  - product: null
- - product: null
  - ExecutesAs
  - launch_role: null
where:
- 'node_type(?portfolio_share) == ResourcePolicy  # AWS::ServiceCatalog::PortfolioShare'
- "node_type(?target_account) == Account          # the sharing (source) account \u2014 account A"
- ?portfolio_share represents the share of ?portfolio to ?target_account
- ?product is in ?portfolio
- '?principal.account != ?target_account       # principal is in a different account (account B)'
- '?launch_role.account == ?target_account     # launch role resides in account A'
- ?principal has EFFECTIVE servicecatalog:ProvisionProduct OR servicecatalog:UpdateProvisionedProduct
  on ?product via the imported portfolio
- ?product has a LAUNCH (not LocalRoleName) constraint, i.e., ConstraintType == LAUNCH and ConstraintParameters.RoleArn
  is in ?target_account
emit:
  source_type: Identity
  target_type: Account
  source: ?principal
  target: ?target_account
  permissions:
  - servicecatalog:ProvisionProduct
  - servicecatalog:UpdateProvisionedProduct
  conditions:
  - trust_relationship
  - iam_permission
  state_logic: ACTIVE when (a) the CrossAccountTrust / PortfolioShare between ?portfolio_share and ?target_account
    is confirmed (DescribePortfolioShares shows the share AND the receiving account has accepted it via
    ImportPortfolio), (b) ?principal has EFFECTIVE servicecatalog:ProvisionProduct or UpdateProvisionedProduct
    on ?product in the imported portfolio, AND (c) ?product has a LaunchRole constraint with a role in
    ?target_account (not a LocalRoleName which resolves in the receiving account). CONDITIONAL(trust_relationship)
    when the portfolio share has been created but the receiving account has not yet accepted/imported
    it (ImportPortfolio not called). CONDITIONAL(parameter_controlled_resource) when the product template
    has no attacker-influenceable execution path (same gate as rule 1). BLOCKED when an SCP in either
    account denies the relevant servicecatalog or CFN actions; or when no LaunchRole constraint is present
    or when the constraint is LocalRoleName (role in receiving account, not cross-account).
  confidence: 0.82
  derived_from:
  - ?portfolio_share CrossAccountTrust ?target_account (PortfolioShare)
  - ?principal CanTrigger ?product
  - ?product ExecutesAs ?launch_role (LaunchRole constraint in ?target_account)
  - can-control.yaml enter-account-via-assume (cross-account foothold pattern)
  false_positive_note: "The FOOTHOLD is in ?target_account (account A, where the launch role resides and\
    \ where CFN deploys resources). The provisioning principal in account B does NOT directly assume the\
    \ launch role \u2014 Service Catalog does. Emit CanEnterAccount (source=?principal in account B, target=?target_account\
    \ A) to reflect that the principal's actions cause resource deployment in account A. Do NOT emit CanAssume\
    \ (the principal never calls sts:AssumeRole on the launch role; it is opaque to them). CRITICAL: A\
    \ LocalRoleName constraint resolves a role in the RECEIVING account (account B), not the sharing account\
    \ (account A) \u2014 omit CanEnterAccount in that case; there is no cross-account foothold via the\
    \ launch role. Only emit when the LaunchRole constraint's RoleArn is confirmed to be in ?target_account\
    \ (not LocalRoleName). For Org-wide shares (OrganizationNode), any principal across all Org member\
    \ accounts with ProvisionProduct access qualifies; prefer emitting CanEnterAccount per account rather\
    \ than CanEnterOrganization unless the share is confirmed Org-wide AND every account's principals\
    \ are accessible to the modeled attacker."
  narrative: '{principal.name} (account B) can provision product {product.name} from a portfolio shared
    by account {target_account.name} (account A, via {portfolio_share.name}). Service Catalog assumes
    the launch role {launch_role.name} (residing in account A) to deploy the product''s CloudFormation
    resources there. {principal.name} gains a foothold in account A through the launch role''s permissions,
    without ever holding sts:AssumeRole on {launch_role.name}.'
move · open · esc close