PrivateLink, shared Amazon Virtual Private Cloud (VPC) subnets, and Transit Gateway separate creation permission from existing network state. This can leave an attacker with a foothold after a temporary access window closes: the endpoint, workload, or attachment already in place may remain usable even after its creation permission is gone. The surviving resource preserves network reachability, not a bypass of application authentication; access still depends on working routes, network controls, and any required application credentials.
Creation permission and connection state
flowchart LR
A([Attacker gets temporary access]) --> C[Create private network path]
C --> P[PrivateLink endpoint]
C --> S[Workload in shared subnet]
C --> T[Transit Gateway attachment]
D([Defender revokes sharing permission]) --> N[Creation grants removed]
D -. existing path survives .-> P
D -. existing path survives .-> S
D -. existing path survives .-> T
P --> O([Traffic can keep flowing])
S --> O
T --> O
class A,D,O principal
class C,P,S,T,N awsResource
| Variation | Revoked control | Surviving object | Required termination action |
|---|---|---|---|
| PrivateLink | The consumer’s allowed-principal entry | An accepted endpoint connection | RejectVpcEndpointConnections or endpoint deletion |
| Shared VPC subnet | The RAM subnet share | Participant-owned workloads and ENIs | Terminate or isolate the participant resources |
| Shared Transit Gateway | The RAM Transit Gateway share | An existing VPC attachment | DeleteTransitGatewayVpcAttachment |
The contractor endpoint survives revocation
A company exposes an internal deployment API through PrivateLink. During a contractor’s migration access window, an attacker compromises a role in the allowlisted account.
The attacker creates an interface endpoint and waits for it to become available, while also obtaining an application credential accepted by the deployment API. When the migration ends, the provider removes the contractor account from the endpoint service’s allowed principals and receives a successful API response.
Removing the contractor’s only effective creation grant blocks another endpoint request, but does not reject the previously accepted connection.[1] API access can continue while the network path and application credential remain usable; a permission-change response alone does not establish either continued access or completed containment.
Conditions for continued access
The attacker must make the network object usable before revocation. That requires:
- Temporary access to create the endpoint, workload, or attachment.
- Enough permission in the participant account to create that exact resource.
- A working route, security group, network ACL, and application protocol.
- Time for the network object to reach a usable state before access is revoked.
The endpoint or attachment preserves only the network path. Application authentication remains in force; valid routes, network controls, and application credentials still determine what can cross it.
PrivateLink endpoint connections
Removing an allowed principal does not reject an endpoint connection that was already accepted. Allowed principals can be accounts, roles, users, or *; removing one entry does not necessarily remove all effective creation permission. Manual acceptance controls connection admission, not application authentication or the termination of previously accepted connections.[1:1]
Run the PrivateLink proof of concept
This assumes the endpoint service uses automatic acceptance and the consumer VPC already has working subnets and security groups. The provider briefly allows the consumer, the consumer creates an endpoint, and the provider removes the permission:
AWS_PROFILE=provider aws ec2 modify-vpc-endpoint-service-permissions \
--service-id "$ENDPOINT_SERVICE_ID" \
--add-allowed-principals "arn:aws:iam::$CONSUMER_ACCOUNT_ID:root"
ENDPOINT_ID=$(AWS_PROFILE=consumer aws ec2 create-vpc-endpoint \
--vpc-endpoint-type Interface \
--vpc-id "$CONSUMER_VPC_ID" \
--service-name "$ENDPOINT_SERVICE_NAME" \
--subnet-ids "$CONSUMER_SUBNET_ID" \
--security-group-ids "$CONSUMER_SECURITY_GROUP_ID" \
--query 'VpcEndpoint.VpcEndpointId' --output text)
until [[ "$(AWS_PROFILE=consumer aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids "$ENDPOINT_ID" \
--query 'VpcEndpoints[0].State' --output text)" == "available" ]]; do
sleep 5
done
AWS_PROFILE=provider aws ec2 modify-vpc-endpoint-service-permissions \
--service-id "$ENDPOINT_SERVICE_ID" \
--remove-allowed-principals "arn:aws:iam::$CONSUMER_ACCOUNT_ID:root"
AWS_PROFILE=consumer aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids "$ENDPOINT_ID" \
--query 'VpcEndpoints[0].{State:State,DNS:DnsEntries[0].DnsName}'
An endpoint accepted before the permission removal can remain available with its DNS entries afterward. This is retained network state, not proof of an authenticated application request. Provider-side rejection or consumer-side endpoint deletion must complete to remove the connection; check individual rejection failures and the resulting endpoint state.[1:2][2]
Resources in shared VPC subnets
VPC subnet sharing lets a participant account place its own resources in a subnet owned by another account. When unsharing completes and no other effective share grants access, the participant loses permission to create new resources. Existing participant resources keep running, and the participant can still describe, modify, and delete them. Managed workflows such as scaling or node replacement may still need continuous access to the shared subnet.[3][4]
The participant-owned workload and its elastic network interface (ENI) preserve the network foothold for as long as they remain active.
Run the shared-subnet proof of concept
This assumes AWS Resource Access Manager sharing with Organizations is enabled, the subnet is non-default, and the participant can use the supplied Amazon Machine Image (AMI) and security group:
SHARE_ARN=$(AWS_PROFILE=owner aws ram create-resource-share \
--name shared-subnet-window \
--resource-arns "$SUBNET_ARN" \
--principals "$PARTICIPANT_ACCOUNT_ID" \
--no-allow-external-principals \
--query 'resourceShare.resourceShareArn' --output text)
INSTANCE_ID=$(AWS_PROFILE=participant aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type t3.nano \
--subnet-id "$SHARED_SUBNET_ID" \
--security-group-ids "$SECURITY_GROUP_ID" \
--query 'Instances[0].InstanceId' --output text)
AWS_PROFILE=participant aws ec2 wait instance-running \
--instance-ids "$INSTANCE_ID"
AWS_PROFILE=owner aws ram disassociate-resource-share \
--resource-share-arn "$SHARE_ARN" \
--resource-arns "$SUBNET_ARN"
AWS_PROFILE=participant aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].State.Name'
Unsharing does not itself stop the instance: it can remain running, retain its ENI, and continue using VPC paths left in place. This inventory state does not prove application activity. AWS also prevents the owner from deleting the subnet or VPC until participant resources are removed.[4:1]
Shared Transit Gateway attachments
A participant can create a VPC attachment while a Transit Gateway is shared through RAM. The owner can later unshare the gateway, but AWS explicitly leaves that attachment functional.[5]
After unsharing, the participant can no longer describe the Transit Gateway. Revocation runbooks must also inspect the attachment inventory.
Run the Transit Gateway proof of concept
This assumes the accounts share an Organization, the Transit Gateway and VPC route tables already carry traffic, and the owner requires manual acceptance for shared attachments:
SHARE_ARN=$(AWS_PROFILE=owner aws ram create-resource-share \
--name transit-gateway-window \
--resource-arns "$TRANSIT_GATEWAY_ARN" \
--principals "$PARTICIPANT_ACCOUNT_ID" \
--no-allow-external-principals \
--query 'resourceShare.resourceShareArn' --output text)
ATTACHMENT_ID=$(AWS_PROFILE=participant \
aws ec2 create-transit-gateway-vpc-attachment \
--transit-gateway-id "$TRANSIT_GATEWAY_ID" \
--vpc-id "$PARTICIPANT_VPC_ID" \
--subnet-ids "$PARTICIPANT_SUBNET_ID" \
--query 'TransitGatewayVpcAttachment.TransitGatewayAttachmentId' \
--output text)
AWS_PROFILE=owner aws ec2 accept-transit-gateway-vpc-attachment \
--transit-gateway-attachment-id "$ATTACHMENT_ID"
until [[ "$(AWS_PROFILE=owner \
aws ec2 describe-transit-gateway-vpc-attachments \
--transit-gateway-attachment-ids "$ATTACHMENT_ID" \
--query 'TransitGatewayVpcAttachments[0].State' \
--output text)" == "available" ]]; do
sleep 5
done
AWS_PROFILE=owner aws ram disassociate-resource-share \
--resource-share-arn "$SHARE_ARN" \
--resource-arns "$TRANSIT_GATEWAY_ARN"
AWS_PROFILE=owner aws ec2 describe-transit-gateway-vpc-attachments \
--transit-gateway-attachment-ids "$ATTACHMENT_ID" \
--query 'TransitGatewayVpcAttachments[0].State'
Unsharing itself does not delete the existing attachment or close its connectivity. Its later state can change through modification or deletion, and routing changes can remove usable paths independently. Reconcile attachment state and routes rather than assuming it remains available until an owner deletion.[5:1][6]
Finding objects left after revocation
Use revocation requests to trigger reconciliation, not as proof of completed revocation. Confirm the intended permission or RAM association change, inspect per-resource failures, and then check the current endpoint, workload, or attachment state. EC2 and RAM management events identify the requests, not downstream application activity.[7][8][9][2:1]
| Event source | Event name | Detection value |
|---|---|---|
ec2.amazonaws.com |
CreateVpcEndpoint |
Identifies a requested endpoint to reconcile with connection inventory. |
ec2.amazonaws.com |
AcceptVpcEndpointConnections |
Requests admission of endpoints; completed connection state and application access remain separate. |
ec2.amazonaws.com |
ModifyVpcEndpointServicePermissions |
Identifies a permission-change request; allowed-principal inventory establishes the actual change. |
ec2.amazonaws.com |
RejectVpcEndpointConnections |
Requests rejection; inspect per-endpoint failures and completed state. |
ram.amazonaws.com |
DisassociateResourceShare |
Requests removal of share associations; reconcile each association’s outcome. |
ec2.amazonaws.com |
RunInstances |
Identifies participant workload creation to resolve against current inventory. |
ec2.amazonaws.com |
CreateTransitGatewayVpcAttachment, DeleteTransitGatewayVpcAttachment |
Identifies attachment lifecycle requests, not necessarily completed creation or deletion. |
For PrivateLink, retain the actor and service identifier from ModifyVpcEndpointServicePermissions. This fragment is attribution and service context only: it identifies neither a removed principal nor a successful removal. Establish the actual change with allowed-principal inventories rather than inferring revocation from the event name.[10][11]
{
"eventSource": "ec2.amazonaws.com",
"eventName": "ModifyVpcEndpointServicePermissions",
"userIdentity": { "arn": "<provider identity ARN>" },
"requestParameters": {
"ModifyVpcEndpointServicePermissionsRequest": {
"ServiceId": "<endpoint service ID>"
}
}
}
Use before-and-after DescribeVpcEndpointServicePermissions inventories to identify which entries were actually removed, then evaluate current effective permission, including account-wide, role, user, and wildcard grants. Without a prior inventory or supported removal evidence, the event above cannot establish a removal. Compare that result with DescribeVpcEndpointConnections: vpcEndpointOwner identifies the endpoint’s owning account, not its creating role or user. Do not equate the provider actor with the consumer or compare an account ID directly with every principal ARN. Identity-specific attribution needs separate creation evidence; account-level inventory alone cannot identify the affected role or user.[1:3][11:1][12][13]
For RAM, resolve the changed share’s resources, principals, and source constraints, including organization or organizational-unit membership and overlapping shares. Check each association for completed DISASSOCIATED state rather than treating DISASSOCIATING or FAILED as effective revocation. Join the affected accounts and resources to current participant ENIs or instances and Transit Gateway attachments, retaining Region and owner-versus-participant account context. An organization trail may record creation in the participant account and revocation in the owner account.[9:1][14]
Track usable objects that remain after the intended revocation is effective until they are removed, contained, or explicitly approved. Use event time and current state, tolerate late or out-of-order events, and revisit the correlation when lifecycle or credible activity evidence changes. A transitional object or a deleted attachment still visible in inventory is not proof of usable access; deleted Transit Gateway attachments remain visible for two hours. Review current routes, network controls, ownership, and application authentication separately from inventory presence, and distinguish surviving network reachability from observed application requests.[6:1]
Completing revocation
Remove or isolate the live network object as well as the sharing permission:
- PrivateLink: reconcile the effective allowed principals and enumerate the affected endpoint connections. Request rejection of unapproved connections, inspect
unsuccessfulresults per endpoint, and confirm completed rejection or deletion before declaring the connection removed.[1:4][2:2] - Shared subnet: inventory participant-owned instances and ENIs before unsharing and arrange termination through the participant account. The subnet owner can describe participant ENIs but cannot otherwise manage them or modify participant-owned security groups. Owner-controlled routes and network ACLs can constrain paths crossing subnet boundaries, with potential impact on other workloads; network ACLs do not filter same-subnet traffic. Use controls on protected destination resources and revoke application credentials where participant resources cannot be promptly removed, then verify the intended paths are contained.[15][16]
- Transit Gateway: delete unapproved VPC attachments and confirm completed deletion, distinguishing
deletingand deleted-but-visible inventory from live attachments. Reconcile route-table associations and propagations as well; an accepted deletion request is not completed containment.[6:2] - Application layer: revoke the API token, certificate, or identity used over the private path. Network access should never be the only authorization check.
References
AWS PrivateLink Guide, Configure an endpoint service. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Amazon EC2 API Reference, RejectVpcEndpointConnections. ↩︎ ↩︎ ↩︎
Amazon VPC User Guide, Share your VPC subnets with other accounts. ↩︎
Amazon VPC User Guide, Unshare a shared subnet. ↩︎ ↩︎
AWS Transit Gateway Guide, Shared transit gateways and unsharing behavior. ↩︎ ↩︎
AWS Transit Gateway Guide, Amazon VPC attachments. ↩︎ ↩︎ ↩︎
Amazon EC2 User Guide, Log Amazon EC2 API calls using AWS CloudTrail. ↩︎
AWS RAM User Guide, Logging AWS RAM API calls using AWS CloudTrail. ↩︎
AWS RAM API Reference, DisassociateResourceShare. ↩︎ ↩︎
detection.wiki, Amazon EC2 CloudTrail event catalog, PrivateLink-related EC2 events. ↩︎
Amazon EC2 API Reference, DescribeVpcEndpointServicePermissions. ↩︎ ↩︎
Amazon EC2 API Reference, DescribeVpcEndpointConnections. ↩︎
Amazon EC2 API Reference, VpcEndpointConnection. ↩︎
AWS RAM API Reference, ResourceShareAssociation. ↩︎
Amazon VPC User Guide, Responsibilities and permissions for owners and participants. ↩︎
Amazon VPC User Guide, Control subnet traffic with network access control lists. ↩︎