{"token_count": 1871}

# Review Identity Access from the CLI

Identity Security is available only with Teleport Enterprise.

`tctl access-review` answers who can reach which resources, what grants that access, and — optionally — whether that access has actually been used. Use it to review and certify role and access-list grants, answer "who can reach this database", or find dormant standing privileges.

You scope every review with a `--query`: a SQL `SELECT` against the Access Graph `access_path` model. The query defines which identities and resources the review covers; the results describe the **access** each identity has to each resource and how that access is granted. It reports access paths only — it does not enumerate other relationships, such as who can *review* an Access List rather than gain access through it.

## Prerequisites

See [Identity Security from the Command Line](https://goteleport.com/docs/identity-security/cli.md) for the shared prerequisites and how the CLI authenticates to Access Graph. This command requires Access Graph v1.30.1 or later, and `tctl access-review` requires a `--query`.

[Identity Activity Center](https://goteleport.com/docs/identity-security/access-graph/identity-activity-center.md) is optional. Without it, the review still reports access paths and their grants, but the [activity columns](#show-access-activity) are omitted and the output notes why.

## Review access

Scope the review with a `--query`. The query is a SQL `SELECT` against `access_path`; the `WHERE` clause is where you narrow it. For example, to review everything one user can reach:

```
$ tctl access-review --query "SELECT * FROM access_path WHERE identity = 'alice'"
```

`identity` matches a user or bot by its stored name or alias — for SSO users this is usually their username or email. `resource` matches a resource by name, and `identity_group` matches what grants access — a role, Access List, or Access Request (shown as **Granted By** in the output). Matches with `=` and `IN` are exact and case-sensitive; use `ILIKE` for case-insensitive or prefix matching when you are unsure of the exact name.

### Common review patterns

| Goal                                       | Query                                                                                          |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| Everything one user can reach              | `SELECT * FROM access_path WHERE identity = 'alice'`                                           |
| Who can reach a resource                   | `SELECT * FROM access_path WHERE resource ILIKE 'prod-db%'`                                    |
| What a role grants, and to whom            | `SELECT * FROM access_path WHERE identity_group IN ('prod-db-admin')`                          |
| What an Access List grants, and to whom    | `SELECT * FROM access_path WHERE identity_group IN ('Prod Admins')`                            |
| Why a resource is reachable through a role | `SELECT * FROM access_path WHERE resource = 'prod-db' AND identity_group IN ('prod-db-admin')` |

Roles and Access Lists are both `identity_group` nodes. The text output does not mark a grantor's kind; to tell a `role` from an `access_list`, use `--format=json` (or `yaml`) and read `granted_by[].node.sub_kind`. When you scope a review through a grantor like this, mind what the results do and do not cover — see [Understand query scope](#understand-query-scope).

By default the output summarizes each identity's resources, resolved access level, the primary grant (the **Granted By** column), and grant counts. Add `--detailed` to break each resource down by each grant and the level it contributes:

```
$ tctl access-review --query "<query>" --detailed
```

**Granted By** is the identity group — a role, Access List, or Access Request — that confers the access; it is what you filter on with `identity_group` in a query.

| Flag            | Description                                                                                                          |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--query`       | Required. SQL `SELECT` against `access_path` scoping the identities to review.                                       |
| `--from`        | Show activity at or after this time. RFC3339, `YYYY-MM-DD`, or durations like `24h`, `7d`. Defaults to 24 hours ago. |
| `--to`          | Upper bound for activity. Defaults to now.                                                                           |
| `--no-activity` | Skip the activity lookup and hide the activity columns for a faster review. Takes priority over `--from`/`--to`.     |
| `--detailed`    | Show each grant with its individual access level instead of summary counts.                                          |
| `--limit`       | Maximum identities to return. Default: `50`.                                                                         |
| `--format`      | `text`, `json`, `yaml`.                                                                                              |

In text output, a `*` marks self-expiring access or a temporary grant.

## Understand query scope

Results are derived only from the paths your query produces. When you scope a review through a given node — a role, an Access List, or any other identity group — the results show only the paths that flow **through that node**, not necessarily every path the matched identities have to the matched resources.

For example, this query answers "what does membership in `Prod Admins` grant, and to whom":

```
$ tctl access-review --query "SELECT * FROM access_path WHERE identity_group IN ('Prod Admins')"
```

It does not tell you the full access of each of those members. A member might also reach the same resource through another role, another Access List, or an approved Access Request, and those paths are invisible to a query scoped to `Prod Admins`.

So when a scoped query shows an identity reaching a resource, it is usually worth confirming that identity's complete access to the resource with a follow-up query scoped by `identity` and `resource` directly:

```
$ tctl access-review --query "SELECT * FROM access_path WHERE identity = 'alice' AND resource = 'prod-server'"
```

This returns every path from `alice` to `prod-server`, with each grant that backs it — the authoritative view to check before you add or remove access.

## Show access activity

If your cluster has [Identity Activity Center](https://goteleport.com/docs/identity-security/access-graph/identity-activity-center.md) enabled, `tctl access-review` shows the activity columns by default over the last 24 hours — how often each access was used and when it was last exercised. Widen the window with `--from` (and optionally `--to`):

```
$ tctl access-review --query "<query>" --from=30d
```

To skip the activity lookup entirely — for a faster, structural-only review of who can reach what — pass `--no-activity`.

Activity counts come from Teleport session-start events, so they cover only resources reached through a Teleport session (SSH, database, Kubernetes, app, desktop). Resources used outside a session — such as AWS, Okta, or GitLab resources — leave the activity columns blank even when the access is used.

## Use tctl access-review with an AI agent

---

TIP

To let an AI agent drive this command, install the matching Agent Skill:

```
$ npx skills add https://github.com/gravitational/teleport/tree/master/skills/teleport-access-review
```

---

## Next steps

- [tctl access-review reference](https://goteleport.com/docs/reference/cli/tctl.md#tctl-access-review)

## Troubleshooting

### access-review is unavailable on this cluster

This usually means Identity Security is not enabled, or the access-review endpoint is not yet available on this cluster. Confirm Identity Security is enabled and that your cluster is v18.11.0 or later.

### column "..." not found

The `WHERE` clause references a column `access_path` does not have. The queryable columns include `identity`, `resource`, `identity_group`, `source`, and `id` — for example, filter users with `identity`, not `identity_name`.

### No access found

An empty result is more often a name mismatch than a true "no access". `=` and `IN` match the stored name exactly and case-sensitively, so re-run with `ILIKE` (for example, `resource ILIKE '%db%'`), read the names from the output, then scope precisely.
