3 minute read

This is a short guide on how to use Steampipe to operationalize AWS Access Advisor.

Access Advisor is a tool within AWS IAM that surfaces last used time of various permissions by an IAM identity (user or role). This is helpful for reconciling access granted with access needed. Over time, permissions inherently accumulate. It is risky to remove permissions from production identities, due to the potential disruption. Access Advisor derisks reducing privilege by highlighting unnecessary grants.

Improvements to Access Advisor

Access Advisor was launched in 2015, and only initially supported last used data at the Service level. In December 2018, AWS added an API. Action level data has since been added, starting with S3 in June 2020. EC2, AWS IAM, and AWS Lambda followed in April 2021.

In the last year, action level data has been rapidly expanded. This included additional support for 140 new services in September, and then 60 more in November.

At re:Invent 2023, AWS launched unused service and action findings for Access Analyzer (further guaranteeing my confusion of the two services) - at $0.20 /IAM role or user analyzed/month.

Limitations

While Access Advisor can provide action level data for hundreds of AWS services, it has some limitations:

  1. Access Advisor supports many, but by no means all AWS services. You can view the full list of supported services at “IAM action last accessed information services and actions.” For example, take AWS’ AI/ML services: Comprehend is not (comprehendmedical is), Sagemaker is not (sagemaker-geospatial is), and Bedrock is not.
  2. Only Management events tend to be included, not Data Events. This limits visibility on data plane access, which can be some of the most critical. Be cautious removing access at the Service level as a result, and instead favor explicitly confirming and removing tracked Action-level access.
  3. Action level data is resource agnostic. Even if an identity only uses an action against a single resource, Access Advisor isn’t sufficient to least privilege that access below resource: "*".
  4. Access Advisor only tracks API calls, and doesn’t distinguish between successful and failed usage. So, an identity regularly and exclusively making calls that are 403’d will be marked as using API’s it may not meaningfully require. (h/t Patrick Sanders)

Steampipe aws_iam_access_advisor

While Access Advisor offers APIs, their ergonomics aren’t immediately fit for purpose at scale. GenerateServiceLastAccessedDetails must be run per-identity, and then GetServiceLastAccessedDetail must be called as a followup.

I want to use Access Advisor to provide:

  1. a global view of over privileging in my environment, and
  2. highlight the most over privileged identities, and
  3. provide a list of actions to remove per-identity

Steampipe, which provides a SQL interface over APIs such as AWS’s, proved a perfect fit for this case.

Note: These queries can be pretty slow in sizable accounts, let me know if you find optimizations!

Queries

This query will produce a sorted list of all principals in the account paired with their number of unused actions:

SELECT principal_arn, COUNT(elem->>'ActionName') AS unused_actions
FROM aws_iam_access_advisor
CROSS JOIN jsonb_array_elements(tracked_actions_last_accessed) AS elem
WHERE elem->>'LastAccessedTime' IS NULL
  AND principal_arn IN (
    SELECT arn
    FROM aws_iam_role
  )
GROUP BY principal_arn
ORDER BY unused_actions DESC;

With a tweak, the query will only include roles that are recently used:

SELECT principal_arn, COUNT(elem->>'ActionName') AS unused_actions
FROM aws_iam_access_advisor
CROSS JOIN jsonb_array_elements(tracked_actions_last_accessed) AS elem
WHERE elem->>'LastAccessedTime' IS NULL
  AND principal_arn IN (
    SELECT arn
    FROM aws_iam_role
    WHERE DATE_TRUNC('day', role_last_used_date) > (CURRENT_DATE - INTERVAL '90 days')::timestamp
  )
GROUP BY principal_arn
ORDER BY unused_actions DESC;

To get the list of actions to remove from a single principal, you can run

SELECT service_name, array_agg(elem->>'ActionName') AS unused_actions
FROM aws_iam_access_advisor
CROSS JOIN jsonb_array_elements(tracked_actions_last_accessed) AS elem
WHERE elem->>'LastAccessedTime' IS NULL
  AND principal_arn = 'FILL_IN_PRINCIPAL_ARN'
ORDER BY service_name DESC;

A Worked Example

To tie things together, here is a case from applying this approach to a production environment.

We wanted to focus on least privileging task roles. To do so, first we found the roles with the highest density of unused actions.

SELECT principal_arn, array_agg(elem->>'ActionName') AS unused_actions
FROM aws_iam_access_advisor
CROSS JOIN jsonb_array_elements(tracked_actions_last_accessed) AS elem
WHERE elem->>'LastAccessedTime' IS NULL
  AND principal_arn IN (
    SELECT arn
    FROM aws_iam_role
    WHERE name ILIKE '%task-role%'
      AND DATE_TRUNC('day', role_last_used_date) > (CURRENT_DATE - INTERVAL '90 days')::timestamp
  )
GROUP BY principal_arn

One thing that immediately jumped out was a high rate of overprivileged Amazon EC2 and Elastic Load Balancer permissions. Reviewing the implicated identities, it quickly jumped out that they all had AmazonEC2ContainerServiceRole attached.

It turns out that this managed IAM policy is phased out, replaced by the Amazon ECS service-linked role. However, this policy was still baked into a core shared ECS Terraform module. The implicit perpetuation of this policy was a downside of Infrastructure as Code.

Removing this policy alone reduced thousands of unused permissions. This one policy was responsible for >50% of unused actions in the environment.