{"id":13576186,"url":"https://github.com/tenable/access-undenied-aws","last_synced_at":"2025-12-13T15:03:51.689Z","repository":{"id":41856857,"uuid":"451608660","full_name":"tenable/access-undenied-aws","owner":"tenable","description":"Access Undenied parses AWS AccessDenied CloudTrail events, explains the reasons for them, and offers actionable remediation steps. Open-sourced by Ermetic.","archived":false,"fork":false,"pushed_at":"2023-01-26T11:04:31.000Z","size":1570,"stargazers_count":264,"open_issues_count":6,"forks_count":11,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-09-30T00:29:49.048Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tenable.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-01-24T19:44:04.000Z","updated_at":"2025-06-18T18:23:14.000Z","dependencies_parsed_at":"2023-02-14T16:45:48.644Z","dependency_job_id":null,"html_url":"https://github.com/tenable/access-undenied-aws","commit_stats":null,"previous_names":["ermetic/aws-access-undenied","ermetic/access-undenied-aws"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/tenable/access-undenied-aws","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenable%2Faccess-undenied-aws","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenable%2Faccess-undenied-aws/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenable%2Faccess-undenied-aws/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenable%2Faccess-undenied-aws/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tenable","download_url":"https://codeload.github.com/tenable/access-undenied-aws/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenable%2Faccess-undenied-aws/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27707677,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-13T02:00:09.769Z","response_time":147,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T15:01:07.795Z","updated_at":"2025-12-13T15:03:51.658Z","avatar_url":"https://github.com/tenable.png","language":"Python","funding_links":[],"categories":["Tools"],"sub_categories":[],"readme":"# Access Undenied on AWS\n\nAccess Undenied parses AWS AccessDenied CloudTrail events, explains the reasons for them, and offers actionable fixes.\n\n[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/noamdahan.svg?style=social\u0026label=Follow%20the%20author)](https://twitter.com/noamdahan)\n\n![Gif demonstrating an example of using AccessUndenied](examples/example.gif)\n\n- [Access Undenied](#access-undenied)\n  - [Overview](#overview)\n    - [Common use cases](#common-use-cases)\n  - [Simple Startup](#simple-startup)\n  - [Installation](#installation)\n    - [Installation from pip](#installation-from-pip)\n    - [Installation from source code (development)](#installation-from-source-code-development)\n  - [Usage](#usage)\n    - [Getting events](#getting-events)\n    - [Permissions](#permissions)\n      - [Same account assets only, no SCPs](#same-account-assets-only-no-scps)\n      - [Cross-account assets and SCPs](#cross-account-assets-and-scps)\n    - [CLI Commands](#cli-commands)\n      - [Analyze](#analyze)\n      - [Get SCPs](#get-scps)\n  - [Output Format](#output-format)\n    - [Output Fields](#output-fields)\n      - [AccessDeniedReason:](#accessdeniedreason)\n      - [ResultDetails](#resultdetails)\n        - [PoliciesToAdd](#policiestoadd)\n        - [ExplicitDenyPolicies](#explicitdenypolicies)\n  - [Acknowledgements](#acknowledgements)\n  - [Appendices](#appendices)\n    - [Running AccessUndenied from a Lambda function](#running-accessundenied-from-a-lambda-function)\n    - [Setting up a venv](#setting-up-a-venv)\n    - [Getting CloudTrail events via the LookupEvents API with the CLI](#getting-cloudtrail-events-via-the-lookupevents-api-with-the-cli)\n    - [Getting Cloudtrail events from the AWS Console's event history](#getting-cloudtrail-events-from-the-aws-consoles-event-history)\n    - [Example Cloudtrail event](#example-cloudtrail-event)\n    - [Least privilege AccessUndenied policy](#least-privilege-accessundenied-policy)\n\n## Overview\n\nAccess Undenied analyzes AWS CloudTrail AccessDenied events, scans the environment to identify and explain the reasons\nfor them, and offers actionable least-privilege remediation suggestions.\n\n### Common use cases\nSometimes, the [new and more detailed AccessDenied messages\nprovided by AWS](https://aws.amazon.com/blogs/security/aws-introduces-changes-to-access-denied-errors-for-easier-permissions-troubleshooting/)\nwill be sufficient. However, that is not always the case.\n1. Some AccessDenied messages do not provide details. Among the\nservices with (many or exclusively) undetailed messages are: S3, SSO, EFS, EKS, GuardDuty, Batch, SQS, and many more.\n2. When the reason for AccessDenied is an explicit deny, it can be difficult to track down \nand evaluate every relevant policy.\n3. Specifically when the reason is an explicit deny in a service control policy (SCP), one has to find and\nevery single policy in the organization that applies to the account.\n4. When the problem is a missing `Allow` statement, AccessUndenied automatically offers a least-privilege\npolicy based on the CloudTrail event.\n## Simple Startup\n\nInstall AccessUndenied:\n```\npip install access-undenied-aws\n```\nAnalyze a CloudTrail event file:\n```\naccess-undenied-aws --file event_history.json\n```\n\n## Installation\n\n### Installation from pip\n\n```\npython -m pip install access-undenied-aws \n```\n\n### Installation from source code (development)\n\nTo install from source code, you can [set up a venv](#setting-up-a-venv) (optionally), and within that venv.\n\n```\npython -m pip install --editable .\n```\n\n## Usage\n\n### Getting events\n\nAccess Undenied works by analyzing a CloudTrail event where access was denied and the error code is either AccessDenied\nor Client.UnauthorizedOperation, it works on an input of one or more CloudTrail events. You can get them from wherever\nyou get events, they can be found in the event history in the console, or by the LookupEvents API, or through whatever\nsystem you use in order to filter and detect events: Athena, Splunk, others. You can either download the records file \n(the default format for multiple events) or just copy and paste a single event. For examples of how to do \nthis: \n- [Getting CloudTrail events via the LookupEvents API with the CLI](#getting-cloudtrail-events-via-the-lookupevents-api-with-the-cli).\n- [Getting Cloudtrail events from the AWS Console's event history](#getting-cloudtrail-events-from-the-aws-consoles-event-history).\n\n### Permissions\n\nAccess Undenied runs with the default permissions of the environment running the cli command, and accepts\nthe `--profile` flag for using a different profile from .aws/credentials. \n```\naccess-undenied-aws --profile my-profile analyze --events-file cloudtrail_events.json\n```\n(note that the location of the profile flag must be before the sub-command (which in this case is `analyze`).\n\nThe role running access-undenied-aws should be granted the appropriate permissions, to do so:\n1. Attach the `SecurityAudit` managed policy.\n2. If you would like to scan cross-account assets and analyze service control policies, attach the following inline policy. This policy allows AccessUndenied to assume roles in your other accounts: \n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"AccessUndeniedAssumeRole\",\n      \"Effect\": \"Allow\",\n      \"Action\": \"sts:AssumeRole\",\n      \"Resource\": [\n        \"arn:aws:iam::\u003cmanagement_account_id\u003e:role/AccessUndeniedRole\",\n        \"arn:aws:iam::\u003caccount_1_id\u003e:role/AccessUndeniedRole\",\n        \"arn:aws:iam::\u003caccount_2_id\u003e:role/AccessUndeniedRole\",\n        \"...\"\n      ]\n    }\n  ]\n}\n```\nIf you do not wish to attach `SecurityAudit`, you may instead attach the updating [least-privilege\n AccessUndenied policy](#least-privilege-accessundenied-policy).\n#### Same account assets only, no SCPs\n\nWhen both the resource and the principal are in the same account as the credentials used to run AccessUndenied and\nService Control Policies (SCPs) do not need to be considered, it is sufficient to just run AccessUndenied with default\ncredentials or a profile, and you do not need to set up any additional profiles.\n\n#### Cross-account assets and SCPs\n\nTo consider assets in multiple accounts and/or SCPs in the management account, we need to set up AWS cross-account roles\nwith the [same policy](#permissions) and the same name as each other (the default is `AccessUndeniedRole`)\n\nwhen setting up these roles, remember to set up the appropriate trust policy (trusting the credentials in the source\naccount, the one you're running AccessUndenied in):\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"AWS\": \"arn:aws:iam::\u003csource_account\u003e:role/AccessUndeniedRole\"\n      },\n      \"Action\": \"sts:AssumeRole\",\n      \"Condition\": {}\n    }\n  ]\n}\n```\nAttach `SecurityAudit` managed policy to the identity , or the updating [least-privilege\n AccessUndenied policy](#least-privilege-accessundenied-policy)\n### CLI Commands\n\nSimplest command\n\n```\naccess-undenied-aws analyze --events-file cloudtrail_events.json\n```\n\nAll options:\n\n```\nOptions:\n  -v, --verbosity LVL  Either CRITICAL, ERROR, WARNING, INFO or DEBUG\n  --profile TEXT       the AWS profile to use (default is default profile)\n  --help               Show this message and exit.\n\nCommands:\n  analyze   Analyzes AWS CloudTrail events and explains the reasons for...\n  get-scps  Writes the organization's SCPs and organizational tree to a file\n```\n\n#### Analyze\nThis command is used to analyze AccessDenied events. It can be used either with the\n`management-account-role-arn` parameter to retrieve SCPs, or with the\n`scp-file` parameter to use a policy data file created by the [get_scps](#get-scps)\ncommand.\n```\nOptions:\n  --events-file FILENAME          input file of CloudTrail events  [required]\n  --scp-file TEXT                 Service control policy data file generated\n                                  by the get_scps command.\n  --management-account-role-arn TEXT\n                                  a cross-account role in the management\n                                  account of the organization, which must be\n                                  assumable by your credentials.\n  --cross-account-role-name TEXT  The name of the cross-account role for\n                                  AccessUndenied to assume. default:\n                                  AccessUndeniedRole\n  --output-file TEXT              output file for results (default: no output\n                                  to file)\n  --suppress-output / --no-suppress-output\n                                  should output to stdout be suppressed\n                                  (default: not suppressed)\n  --help                          Show this message and exit.\n\n```\n**Example:**\n```\naccess-undenied-aws analyze --events-file events_file.json\n```\n#### Get SCPs\nThis command is used to writes the organization's SCPs and organizational tree\nto an organizational policy data file. This command should be run from the management\naccount.\n```\nOptions:\n  --output-file TEXT  output file for scp data (default: scp_data.json)\n  --help              Show this message and exit.\n```\n**Example:**\n```\naccess-undenied-aws get-scps\n```\nThen when running analyzing (from the same account or a different account)\n```\naccess-undenied-aws analyze --events-file events_file.json --scp-file scp_data.json\n```\n\n## Output Format\n```json\n{\n  \"EventId\": \"55555555-12ad-4f70-9140-d44428038119\",\n  \"AssessmentResult\": \"Missing allow in an identity-based policy\",\n  \"ResultDetails\": {\n    \"PoliciesToAdd\": [\n      {\n        \"AttachmentTargetArn\": \"arn:aws:iam::123456789012:role/MyRole\",\n        \"Policy\": {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Action\": \"rds:DescribeDBInstances\",\n              \"Resource\": \"arn:aws:rds:ap-northeast-3:123456789012:db:*\"\n            }\n          ]\n        }\n      }\n    ]\n  }\n}\n```\nThis output for example, tells us that access was denied because of there is no \n`Allow` statement in an identity-based policy.\nTo  remediate, we should attach to the IAM role \n`arn:aws:iam::123456789012:role/MyRole` the policy:\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"rds:DescribeDBInstances\",\n      \"Resource\": \"arn:aws:rds:ap-northeast-3:123456789012:db:*\"\n    }\n  ]\n}\n```\n### Output Fields\n#### AccessDeniedReason:\nThe reason why access was denied. Possible Values\n\nMissing allow in:\n* Identity policy\n* Resource policy (in cross-account access)\n* Both (in cases of cross-account access)\n* Permissions boundary\n* Service control policy (with allow-list SCP strategy)\n\nExplicit deny from:\n* Identity policy\n* Resource policy\n* Permissions boundary\n* Service control policy\n\nInvalid action:\n* a principal or action that cannot be simulated by access undenied.\n\n\"Allowed\"\nAn `\"Allowed\"` result means that access undenied couldn't find the reason \nfor AccessDenied, this could be for a variety of reasons:\n* Policies, resources and/or identities have changed since the CloudTrail event and access \nnow actually allowed\n* Unsupported resource policy type\n* Unsupported policy type (VPC endpoint policy, session policy, etc.)\n* Unsupported condition key\n#### ResultDetails\nThese are the details of the result, explaining the remediation steps, \nthis section may contain either `PoliciesToAdd` or `ExplicitDenyPolicies`.\n##### PoliciesToAdd\nThese are the policies which need to be added to enable least-privilege access.\nEach policy contains: \n* `AttachmentTargetArn`: the entity to which the new policy\nshould be attached\n* `Policy`: The content of the policy to be added\n##### ExplicitDenyPolicies\nThese are the policies cause explicit deny, which need to be removed or\nmodified to facilitate access. AccessUndenied also gives the specific\nstatement causing the `Deny` outcome.\n* `AttachmentTargetArn`: the entity to which the policy causing explicit\ndeny is currently attached\n* `PolicyArn`: The arn (if applicable) of the policy causing explicit deny.\nFor the sake of convenience, resource policies are represented by generic \nplaceholder arns such as: `arn:aws:s3:::my-bucket/S3BucketPolicy`\n* `PolicyName`: The policy name, if applicable. Resource policies\nare represented by generic placeholder names such as `S3BucketPolicy`\n* `PolicyStatement`: The specific statement in the aforementioned policy\ncausing explicit deny\n## Acknowledgements\nThis project makes use of Ian Mckay's [iam-dataset](https://github.com/iann0036/iam-dataset) Ben\nKehoe's [aws-error-utils](https://github.com/benkehoe/aws-error-utils).\n## Appendices\n### Running AccessUndenied from a Lambda function\n[Full README here](examples/lambda/README.md)\n### Setting up a venv\n```\npython -m venv .venv\n```\n\n| Platform   |      Shell      | Command to activate virtual environment |\n|------------|-----------------|-----------------------------------------|\n| POSIX      | bash/zsh        | $ source .venv/bin/activate            |\n|            | fish            | $ source .venv/bin/activate.fish       |\n|            | csh/tcsh        | $ source .venv/bin/activate.csh        |\n|            | PowerShell Core | $ .venv/bin/Activate.ps1               |\n| Windows    | cmd.exe         | C:\\\u003e .venv\\Scripts\\activate.bat        |\n|            | PowerShell      | PS C:\\\u003e .venv\\Scripts\\Activate.ps1     |\n\n### Getting CloudTrail events via the LookupEvents API with the CLI\nThis section is directly based on this [AWS support page](https://aws.amazon.com/premiumsupport/knowledge-center/troubleshoot-iam-permission-errors/).\nIt has been adapted so that the command outputs raw events rather than an ascii table.\n1.    Run the following AWS CLI command:\n```\naws cloudtrail lookup-events --start-time \"yyyy-mm-ddThh:mm:ss+0000\" --end-time \"yyyy-mm-ddThh:mm:ss+0000\" \\\n  --query \"Events[*].CloudTrailEvent\" --output text | jq -r \". | \\\n  select(.userIdentity.arn == \\\"arn:aws:sts::123456789012:assumed-role/role-name/role-session-name\\\" \\\n  and .eventType == \\\"AwsApiCall\\\" and .errorCode != null \\\n  and (.errorCode | ascii_downcase | (contains(\\\"accessdenied\\\") or contains(\\\"unauthorized\\\"))))\" | \\\n  jq -s '{Records:.}' \u003e lookup_events_output.json\n```\n\u003e Note: The rate of lookup requests to CloudTrail is limited to one request per second per account. If you exceed this limit, then a throttling error occurs.\n\nYou can get errors for all users by removing this line:\n```\n.userIdentity.arn == \\\"arn:aws:sts::123456789012:assumed-role/role-name/role-session-name\\\" and\n```\n\nThe command outputs errors to `lookup_events_output.json`, which can be analyzed by Access Undenied\n(using additional parameters as needed).\n```\naccess-undenied-aws analyze --events-file lookup_events_output.json\n```\n\n### Getting CloudTrail events from the AWS Console's event history\n\n1. Open the AWS console\n2. Go to \"CloudTrail\"\n3. In the sidebar on the left, click Event History\n4. Find the event you're interested in checking. Unfortunately, the console doesn't let you filter by ErrorCode, so\n   you'll have to filter some other way, e.g. by username or event name.\n5. Download the event:\n    1. By clicking the event, copying the event record, and pasting it to a json file locally. or,\n    2. By clicking download events -\u003e download as JSON in the top-right corner. (Access Undenied will handle all events\n       where the ErrorCode is AccessDenied or Client.UnauthorizedOperation)\n\nWith the event saved locally, you may use the [cli command](#cli-arguments)\n\n### Example Cloudtrail event\n\nOne event in file:\n\n```json\n{\n  \"awsRegion\": \"us-east-2\",\n  \"eventID\": \"5ac7912b-fd5d-436a-b60c-8a4ec1f61cdc\",\n  \"eventName\": \"ListFunctions20150331\",\n  \"eventSource\": \"lambda.amazonaws.com\",\n  \"eventTime\": \"2021-09-09T14:01:22Z\",\n  \"eventType\": \"AwsApiCall\",\n  \"userIdentity\": {\n    \"accessKeyId\": \"ASIARXXXXXXXXXXXXXXXX\",\n    \"accountId\": \"123456789012\",\n    \"arn\": \"arn:aws:sts::123456789012:assumed-role/RscScpDisallow/1631196079303620000\",\n    \"principalId\": \"AROARXXXXXXXXXXXXXXXX:1631196079303620000\",\n    \"sessionContext\": {\n      \"attributes\": {\n        \"creationDate\": \"2021-09-09T14:01:20Z\",\n        \"mfaAuthenticated\": \"false\"\n      },\n      \"sessionIssuer\": {\n        \"accountId\": \"123456789012\",\n        \"arn\": \"arn:aws:iam::123456789012:role/RscScpDisallow\",\n        \"principalId\": \"AROARXXXXXXXXXXXXXXXX\",\n        \"type\": \"Role\",\n        \"userName\": \"RscScpDisallow\"\n      },\n      \"webIdFederationData\": {}\n    },\n    \"type\": \"AssumedRole\"\n  },\n  \"errorCode\": \"AccessDenied\",\n  \"errorMessage\": \"User: arn:aws:sts::123456789012:assumed-role/RscScpDisallow/1631196079303620000 is not authorized to perform: lambda:ListFunctions on resource: * with an explicit deny\",\n  \"sourceIPAddress\": \"xxx.xxx.xxx.xxx\",\n  \"readOnly\": true,\n  \"eventVersion\": \"1.08\",\n  \"userAgent\": \"aws-cli/2.2.16 Python/3.8.8 Linux/4.19.128-microsoft-standard exe/x86_64.ubuntu.20 prompt/off command/lambda.list-functions\",\n  \"requestID\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx\",\n  \"managementEvent\": true,\n  \"recipientAccountId\": \"123456789012\",\n  \"eventCategory\": \"Management\"\n}\n```\n\nMultiple events in file:\n\n```json\n{\n  \"Records\": [\n    {\n      \"awsRegion\": \"us-east-1\",\n      \"eventID\": \"xxxxxxxx-xxxx-xxxx-xxxx-8234c1555c12\"\n      //... rest of cloudtrail_event ...\n    },\n    {\n      //... another cloudtrail_event ...\n    }\n    // more events...\n  ]\n}\n```\n\n### Least privilege AccessUndenied policy\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"AccessUndeniedLeastPrivilegePolicy\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"ecr:GetRepositoryPolicy\",\n        \"iam:Get*\",\n        \"iam:List*\",\n        \"iam:SimulateCustomPolicy\",\n        \"kms:GetKeyPolicy\",\n        \"lambda:GetPolicy\",\n        \"organizations:List*\",\n        \"organizations:Describe*\",\n        \"s3:GetBucketPolicy\",\n        \"secretsmanager:GetResourcePolicy\",\n        \"sts:DecodeAuthorizationMessage\"\n      ],\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenable%2Faccess-undenied-aws","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftenable%2Faccess-undenied-aws","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenable%2Faccess-undenied-aws/lists"}