- What: Tool for visualizing AWS attack paths
- Impact: Security professionals analyzing AWS environments
Daniel Heinsen and I have spent a lot of our time trying to answer one question inside AWS environments, usually in a hurry: starting from the access I have right now, where can I actually end up? That turns out to be a hard question to answer in AWS. iam:SimulatePrincipalPolicy will evaluate one action, against one principal, in one account, one call at a time. What it can’t tell you is that the low-privilege user you just landed on can update a Lambda function, inherit that function’s execution role, send an SSM command to an EC2 instance, read an external ID out of Parameter Store, and assume into the next account over. That is the shape of nearly every real AWS path I have worked. No single hop in it is interesting on its own, which is exactly why it tends to survive a policy review. AWSHound is our attempt at making that chain visible. AWSHound is a read-only collector that turns an AWS account or an entire AWS Organization into a BloodHound Community Edition (BHCE) OpenGraph dataset. Before it draws a single edge, it runs an offline identity and access management (IAM) policy evaluation, such that an edge only exists when a principal’s effective permissions actually resolve to Allow once service control policies (SCPs), resource control policies (RCPs), and permissions boundaries have all had their say. Once the offline policy evaluation creates all nodes and edges, BloodHound can leverage its built-in path finding to map attack pathways from one AWS principal to another. AWSHound is free, self-hosted, and runs on both BloodHound Community Edition and BloodHound Enterprise. Check it out here: https://github.com/AWSHound/AWSHound . The Problem Attackers and defenders have the same bad time in cloud environments, it is genuinely hard to tell which attack paths are real. AWS is the worst offender I have worked with and it’s not simply because “IAM is complicated.” The reason is that a permission in AWS is not a property of a principal. It’s the answer to a four-part question: Can this principal perform this action on this resource in this request context? Change any one of the four and the answer can flip. Nothing in the console or command-line interface (CLI) holds all four at once. Even for one of those questions, the answer gets assembled out of a stack of policy documents that live in different places and are usually owned by different teams: The principal’s policies, which can include: Inline Policy Managed Policies Inherited Policies from Group Membership Permission Boundaries, which never grant anything and only take away Service Control Policies consolidated and then intersected across the root management account, organizations OUs, and individual accounts Resource-Specific Policies, such as: S3 Bucket Policy Key Policy Function Policy Role Trust Policy etc. The boundary and the organization policies only ever narrow the funnel. Neither one can grant anything on its own. Policy Evaluation Here is the part I genuinely did not appreciate until we started writing the AWS policy evaluator. There is no single rule for combining the identity side with the resource side. It changes per service. Service Same-account behavior Cross-account behavior STS identity AND trust policy identity AND trust policy S3 identity OR bucket policy identity AND bucket policy Lambda identity OR function policy identity AND function policy EC2, SSM, CloudFormation, and EKS are absent from that table because they are identity governed. They check the identity side and nothing else. What is left is the set of services where a second policy, owned by somebody else, gets a vote. The row worth a footnote is the first one as assuming a role needs both halves even inside a single account. KMS is absent for a different reason. It does not fit the pattern at all. It runs three layers in order, all of them anchored on the key policy, and the first layer to match wins. The key policy itself: If it matches the request directly, that settles it, and a deny beats an allow. Account root delegation: If the key policy hands authority to the account root with something broad like kms:* , an effective IAM allow can grant the action, and an IAM deny blocks it. A KMS grant: A collected grant can authorize the operations it names, and we keep the grant constraints on the edge so you can see what they were. Two things follow from that. A key policy we could not collect fails closed rather than open, which is the opposite of how we treat an unknown condition everywhere else. AWSHound’s KMS collection is also limited to same-account only today. Key policies naming a principal from Account A that names a principal within Account B will fail to produce an edge. So “Can this role read that bucket?” and “Can this role decrypt with that key?” are not the same shape of question. Anything that answers one of them with the other is guessing. Conditions make it even harder. A policy can make a grant conditional on things that do not exist until a real reque...