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.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
CrossAccountTrust {'target_account': None}
{'principal': None} CanTrigger {'product': None}
{'product': None} ExecutesAs {'launch_role': None}
where
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 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). |
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}.'