AWS IAM Identity Center Identity Security Posture Management (ISPM)
Console-only posture scanning for AWS IAM Identity Center: 25 weighted permission-set and Identity Store checks, run from the dashboard Posture tab and scored 0-100.
Overview
PasskeyBridge reads an AWS IAM Identity Center instance through the SSO Admin and Identity Store APIs, runs 25 checks against its permission sets, account assignments, users and groups, and stores each one as a finding with a weight, a status and evidence. The dashboard renders a 0-100 posture score and the findings grouped by category. Every API action the scan calls is a read.
This module audits permission-set and assignment hygiene. It runs no MFA, IAM user, password policy or root account checks, and it never calls AWS Organizations: account coverage is derived from the accounts that permission sets are already provisioned to.
It lives in the dashboard Posture tab, under the AWS IAM Identity Center provider tab. Scan rows carry provider set to aws_idc.
Who can run it. Posture is an Enterprise tab: on the starter and pro plans the dashboard shows an upgrade panel in place of the tab. The plan gate is in the dashboard only. The shield-aws-idc-ispm function enforces something different: the caller must present a valid dashboard session and be an admin member of the tenant in shield_tenant_members.
Console-only. shield-aws-idc-ispm is not in the Cloudflare worker's PUBLIC_FUNCTIONS list, so it has no https://api.passkeybridge.io/v1 route and no API key lane. The dashboard calls it directly with supabase.functions.invoke and your session JWT.
Credentials and dashboard fields
In AWS. Create an IAM user with programmatic access in the account that hosts your IAM Identity Center instance, and attach a read-only policy with the actions in the next section. The scan signs every request with SigV4 and talks to exactly two hosts in the region you configure: sso.REGION.amazonaws.com for SSO Admin and identitystore.REGION.amazonaws.com for the Identity Store.
In the dashboard. Posture tab, AWS IAM Identity Center provider tab, expand AWS IAM Identity Center configuration.
| Field | Goes to | Stored as |
|---|---|---|
| AWS region | aws_region on shield_tenants | Clear text. Defaults to us-east-1 and must have that shape: two letters, a word, a digit. |
| Access key ID | aws_access_key_id on shield_tenants | Clear text in the column. Column-level SELECT is revoked from the authenticated and anon roles. |
| Secret access key | aws_secret_access_key_encrypted on shield_tenants | AES-256-GCM under the server-held SHIELD_ENCRYPTION_KEY. |
Press Save. All three fields are required on every save.
{ "action": "save_aws_idc_config", "tenant_id": "<tenant uuid>", "aws_region": "us-east-1", "aws_access_key_id": "AKIA...", "aws_secret_access_key": "<secret>" }A successful save answers {"ok": true} and writes a save_aws_idc_config audit row.
| Status | Body |
|---|---|
| 400 | aws_region, aws_access_key_id, and aws_secret_access_key are all required |
| 400 | Invalid AWS region format (e.g., us-east-1) |
| 500 | Failed to save provider configuration |
The region matters. The scan looks for an IAM Identity Center instance in that region only. If ListInstances comes back empty, the scan completes with a single failed check, IAM Identity Center Instance, and a score of 0.
Required IAM permissions
These are the only actions the scan calls. All of them are reads.
| Service | Actions |
|---|---|
sso (SSO Admin) | ListInstances, ListPermissionSets, DescribePermissionSet, GetInlinePolicyForPermissionSet, ListManagedPoliciesInPermissionSet, GetPermissionsBoundaryForPermissionSet, ListAccountsForProvisionedPermissionSet, DescribeInstanceAccessControlAttributeConfiguration |
identitystore | ListUsers, ListGroups, ListGroupMemberships, ListGroupMembershipsForMember |
The SigV4 service name for SSO Admin is sso and the target prefix is SWBExternalService; the Identity Store target prefix is AWSIdentityStore.
Pagination and sampling. Lists follow NextToken for up to 10 pages of 100 items. Permission sets are described in batches of 5. The orphaned-user check samples the first 20 users and the oversized-group check the first 10 groups; if AWS throttles either one, that check is recorded as skipped rather than reporting a wrong percentage. Any permission set whose DescribePermissionSet call failed is dropped from the analysed set and counted in the sprawl check's evidence as failed_describe.
Checks by category
25 checks run on every scan, in seven categories.
| Category | Checks | What they read |
|---|---|---|
| permission_sets | 9 | Permission set count over 25, AdministratorAccess and PowerUserAccess policies, inline policies, permission boundaries on admin sets, unassigned sets, more than 10 managed policies on one set, the customer-managed share of attached policies, and broad AWS-managed policies such as ReadOnlyAccess or SecurityAudit. |
| authorization | 5 | Whether attribute-based access control is configured, how many accounts have any assignment, break-glass scoping of admin sets, total assignment volume, and accounts holding more than 10 permission sets. |
| identity_store | 4 | User count, group count, the sampled share of users with no group membership, and groups with more than 50 members. |
| configuration | 2 | Whether the instance was discovered, and how many permission sets carry a relay state URL. |
| session | 2 | Permission sets whose session runs longer than 4 hours, and whether every set still uses the 1-hour default. |
| governance | 2 | Naming convention and description coverage across permission sets. |
| authentication | 1 | Identity source health, measured as the number of users in the Identity Store. |
Assignment coverage is relative. With no Organizations call, "accounts with assignments" means the distinct account ids that permission sets are provisioned to. A single-account estate warns; it cannot be compared against the total number of accounts in your organization.
Attribute-based access control is three-valued. The check passes when DescribeInstanceAccessControlAttributeConfiguration succeeds, fails when AWS says the configuration does not exist, and is skipped when the call errors for any other reason, so a permissions problem does not read as a finding.
Posture score
The score is a penalty ratio over the checks that actually ran. Every check carries a hard-coded weight and a score_impact: zero when it passes, a negative number when it fails or warns. The function computes, over all non-skipped checks, 100 x (1 - total_penalty / max_penalty), where total_penalty is the sum of the absolute score_impact values and max_penalty is the sum of weight x 2. It rounds it and clamps it to 0-100. Weights run from 1 to 10. The heaviest AWS checks are the instance discovery check at 10, then identity source health and the AdministratorAccess check at 8.
There are no severity multipliers. severity (critical, high, medium, low, info) is a label on the finding row and has no effect on the arithmetic.
Skipped checks leave both sides of the ratio, so a check that could not run neither helps nor hurts. If every check is skipped the score is 0.
The tab turns the number into one of four labels.
| Score | Label in the tab |
|---|---|
| 80-100 | Strong |
| 60-79 | Moderate |
| 40-59 | Needs work |
| 0-39 | Critical |
Credential and result storage
Credentials. Only the secret access key is encrypted, with AES-256-GCM under SHIELD_ENCRYPTION_KEY, and it is decrypted inside the function for the length of one scan. Encryption is mandatory: a save fails rather than storing plaintext if the key is missing. The region and access key ID are stored in clear text because SigV4 needs them, and a migration revokes column-level SELECT on both aws_access_key_id and aws_secret_access_key_encrypted from the authenticated and anon roles.
Findings. Evidence holds counts and permission set names, for example admin_ps_names, long_session_ps or overloaded accounts with their assignment counts. Identity Store user records are counted and sampled inside the function and are not stored.
Scan rows land in shield_posture_scans and one finding row per check in shield_posture_findings. Both tables are tenant-admin read-only under row-level security: insert, update and delete are denied to the authenticated role, and the function writes them with its service-role client.
Audit trail. A completed scan writes a run_aws_idc_posture_scan row to shield_audit_log with the provider, score, check counts and scan id. A failed scan writes the same verb with result set to failure.
Running a scan and retrying failures
Scans run on demand. No cron job calls any ISPM function, so a scan happens when someone presses a button in the Posture tab. Open the tab, select AWS IAM Identity Center, and press Run scan in the tab header. The button is disabled while a scan is in flight and the tab reloads the scan list when it finishes.
The button invokes the function with your signed-in session.
const { data, error } = await supabase.functions.invoke("shield-aws-idc-ispm", {
body: { action: "run_scan", tenant_id: "<tenant uuid>" },
});A completed scan answers with the summary the tab renders.
{
"scan_id": "7c1f0b2e-...",
"parent_scan_id": null,
"posture_score": 68,
"checks_total": 25,
"checks_passed": 14,
"checks_failed": 5,
"checks_warning": 4,
"checks_skipped": 2,
"duration_ms": 8421,
"partial_failures": []
}Retry failed checks appears in the Findings header once a completed scan has at least one failed or warning check. It creates a second scan that re-runs only the failed and warning check ids and copies the passed and skipped findings forward, so a skipped check changes only on a full Run scan.
{ "action": "retry_failed_checks", "tenant_id": "<tenant uuid>", "retry_of_scan_id": "<scan uuid>" }| Status | Body | Cause |
|---|---|---|
| 401 | Missing authorization header or Unauthorized | No session JWT, or it has expired. |
| 400 | Invalid JSON body | The body did not parse as JSON. |
| 400 | tenant_id required | tenant_id was missing from the body. |
| 403 | Forbidden: tenant admin access required | The signed-in user is not an admin member of that tenant. |
| 500 | Failed to verify tenant access | The membership lookup itself failed. |
| 400 | Unknown action | action was not a save action, run_scan or retry_failed_checks. |
| 400 | AWS IAM Identity Center configuration not found. Configure your AWS region, Access Key ID, and Secret Access Key first. | No credentials are stored for this tenant. |
| 400 | retry_of_scan_id required | Retry was called without a parent scan id. |
| 404 | parent scan not found | No scan row has that id. |
| 403 | parent scan does not belong to this tenant/provider | The parent scan belongs to another tenant or another provider. |
| 400 | no failed or warning checks to retry | Every check in the parent scan passed or was skipped. |
| 500 | Scan failed: ... | The scan threw. The scan row is stored with status failed and the thrown message. |
Confirm it worked. The score card shows the band, the score out of 100, and the Passed, Failed and Warnings tiles. The card under it shows Scan, Duration, Started and Type, where Type reads Full scan or Retry, N checks. Findings are grouped by category below; expanding one shows its description, Remediation, Evidence JSON, and a line reading Weight, impact and check with that check's numbers and id. The Alerts section in the same tab can post a completed scan to a Slack webhook or to email recipients once its failed-check threshold is met.
Related from the blog
- Account Recovery Is the Front Door: Carrier-State Checks in Credit Union and Community Bank Recovery Flowssecurity · 11 min read
- Roaming Steering and Identity: When the HPLMN You Trust Hands You to a VPLMN You Don'tsecurity · 20 min read
- Network Slicing in 5G Standalone and the Birth of Per-Slice Identity Posturentn · 15 min read