User Access Audit Template
A template for reviewing and certifying user access rights across systems, applications, and data repositories.
Overview
A user access audit verifies that every user has the right level of access to systems, applications, and data. It is a core control for identity governance, least privilege, and compliance with standards like SOC 2, ISO 27001, and PCI-DSS. This template provides a structured way to collect access data, review permissions, certify access, and remediate findings.
When to Use
- Performing quarterly or annual access reviews.
- Preparing for a compliance audit or certification.
- After a role change, reorganization, or merger.
- When privileged access is suspected to be excessive.
- After offboarding a user or removing a contractor.
Prerequisites
- An identity source such as an SSO provider or identity management system.
- A list of applications, systems, and data repositories under review.
- Owners or managers for each application who can certify access.
- A defined access review schedule and escalation process.
Solution
Template
1. Audit Scope
| Scope Item | Description |
|---|---|
| Period | 2026-Q2 |
| Systems reviewed | AWS, GitHub, Jira, Confluence, Slack, VPN, Google Workspace |
| Population | Employees, contractors, service accounts, privileged admin roles |
| Reviewers | Application owners, managers, security team |
| Due date | 2026-07-15 |
| Exceptions allowed | Yes, with risk acceptance and expiration |
2. Identity Inventory
| User ID | Name | Type | Department | Status | Last Reviewed |
|---|---|---|---|---|---|
alice@example.com | Alice Chen | Employee | Engineering | Active | 2026-03-31 |
bob@example.com | Bob Smith | Contractor | Finance | Active | 2026-03-31 |
| svc-api-prod | API Service | Service account | Platform | Active | 2026-05-15 |
carol@example.com | Carol Jones | Employee | Marketing | Inactive | 2026-01-31 |
3. Access Mapping
| User | System | Role / Permission | Business Justification | Reviewer | Decision |
|---|---|---|---|---|---|
alice@example.com | AWS | PowerUser | Manages infrastructure | Platform lead | Keep |
bob@example.com | GitHub | Read | Reviews pull requests | Engineering manager | Keep |
alice@example.com | Jira | Admin | Configures workflows | IT lead | Revoke |
| svc-api-prod | AWS | S3 read-only | Application reads reports | Platform lead | Keep |
carol@example.com | Slack | Member | Left company | HR | Revoke |
4. Privileged Access Review
| User | System | Privileged Role | Justification | Risk | Reviewer | Decision |
|---|---|---|---|---|---|---|
alice@example.com | AWS | Root access | Emergency break-glass | High | CISO | Keep with MFA |
dave@example.com | GitHub | Organization owner | Manages repositories | High | CTO | Keep |
eve@example.com | VPN | Full tunnel | Remote admin access | High | Security lead | Revoke |
5. Certification Log
| Application | Reviewer | Status | Date | Notes |
|---|---|---|---|---|
| AWS | Platform lead | Certified | 2026-07-10 | 2 revocations pending |
| GitHub | CTO | Certified | 2026-07-08 | 1 orphan account removed |
| Jira | IT lead | In progress | 2026-07-05 | Admin role under review |
| Slack | HR | Certified | 2026-07-09 | 3 inactive accounts revoked |
6. Remediation Plan
| Finding | Action | Owner | Due Date | Status |
|---|---|---|---|---|
| Excessive admin rights in Jira | Downgrade to user | IT lead | 2026-07-20 | Open |
| Inactive Slack account | Deactivate | HR | 2026-07-12 | Done |
| Orphaned service account | Investigate and disable | Platform team | 2026-07-18 | Open |
| Missing MFA on privileged users | Enforce MFA | IAM team | 2026-07-15 | In progress |
Explanation
The template connects identities to permissions, business justification, and accountable reviewers. Without this structure, organizations accumulate stale accounts and over-privileged users, increasing both insider risk and external attack surface. Regular access reviews are required by most security frameworks and are a practical way to enforce least privilege.
Variants
- Application-specific access review: Focuses on one system, such as AWS IAM or GitHub organization access.
- Privileged access review: Only reviews admin, root, or emergency access accounts.
- Service account audit: Reviews non-human identities and their API keys or credentials.
- Contractor access review: Time-bound review for external users with temporary access.
- Data access audit: Focuses on users who can access sensitive databases, data lakes, or analytics tools.
What Works
- Automate identity collection from the SSO or identity provider.
- Send reminders to reviewers before the due date.
- Require business justification for every privileged role.
- Revoke access immediately when a user changes role or leaves.
- Schedule quarterly reviews for privileged access and annual reviews for general access.
- Document risk acceptance for necessary exceptions.
- Track remediation until every finding is closed.
Common Mistakes
- Reviewing access only once a year without follow-up.
- Letting managers keep access for employees who changed roles.
- Ignoring service accounts and shared credentials.
- Skipping privileged access or emergency break-glass accounts.
- Not linking access decisions to business justification.
- Failing to verify that revocations actually happened.
- Storing review evidence in scattered emails or documents.
Advanced Solutions
Automated access review with Okta API
Pull user access data from Okta and generate a review report automatically:
import requests
import csv
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List
@dataclass
class UserAccessRecord:
user_id: str
user_name: str
status: str
last_login: str
assigned_apps: List[str]
admin_roles: List[str]
class OktaAccessReviewer:
def __init__(self, api_token: str, domain: str):
self.headers = {
"Authorization": f"SSWS {api_token}",
"Accept": "application/json",
}
self.base_url = f"https://{domain}/api/v1"
def get_inactive_users(self, days: int = 90) -> List[dict]:
"""Find users who haven't logged in within the specified period."""
cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat() + "Z"
users = []
params = {"filter": f'status eq "ACTIVE"'}
resp = requests.get(
f"{self.base_url}/users",
headers=self.headers,
params=params,
)
for user in resp.json():
last_login = user.get("lastLogin")
if last_login and last_login < cutoff:
users.append({
"id": user["id"],
"email": user["profile"]["email"],
"last_login": last_login,
"status": user["status"],
})
return users
def get_user_apps(self, user_id: str) -> List[str]:
"""Get applications assigned to a user."""
resp = requests.get(
f"{self.base_url}/users/{user_id}/appLinks",
headers=self.headers,
)
return [app["label"] for app in resp.json()]
def get_admin_roles(self, user_id: str) -> List[str]:
"""Get admin roles assigned to a user."""
resp = requests.get(
f"{self.base_url}/users/{user_id}/roles",
headers=self.headers,
)
return [role["type"] for role in resp.json()]
def generate_review_report(self, output_file: str) -> None:
"""Generate a CSV report of all active users and their access."""
resp = requests.get(
f"{self.base_url}/users",
headers=self.headers,
params={"filter": 'status eq "ACTIVE"'},
)
with open(output_file, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"User ID", "Email", "Status", "Last Login",
"Assigned Apps", "Admin Roles"
])
for user in resp.json():
apps = self.get_user_apps(user["id"])
roles = self.get_admin_roles(user["id"])
writer.writerow([
user["id"],
user["profile"]["email"],
user["status"],
user.get("lastLogin", "Never"),
"; ".join(apps),
"; ".join(roles),
])
# Example usage
reviewer = OktaAccessReviewer(api_token="YOUR_TOKEN", domain="yourorg.okta.com")
inactive = reviewer.get_inactive_users(days=90)
for u in inactive:
print(f"INACTIVE: {u['email']} - last login: {u['last_login']}")
reviewer.generate_review_report("access_review_q2.csv")
AWS IAM access analyzer for automated finding detection
Use AWS IAM Access Analyzer to detect unused permissions and cross-account access:
#!/bin/bash
set -euo pipefail
# Create an analyzer if it doesn't exist
ANALYZER_NAME="org-access-analyzer"
aws accessanalyzer create-analyzer \
--analyzer-name "$ANALYZER_NAME" \
--type ORGANIZATION \
--region us-east-1
# List all findings
echo "=== Active Findings ==="
aws accessanalyzer list-findings \
--analyzer-arn "$(aws accessanalyzer list-analyzers --query 'analyzers[0].arn' --output text)" \
--filter '{"status":{"eq":["ACTIVE"]}}' \
--query 'findings[*].{id:id,resource:resource.resourceArn,type:findingType,createdAt:createdAt}' \
--output table
# Export findings to CSV for audit trail
aws accessanalyzer list-findings \
--analyzer-arn "$(aws accessanalyzer list-analyzers --query 'analyzers[0].arn' --output text)" \
--query 'findings[*].{id:id,resource:resource.resourceArn,type:findingType,createdAt:createdAt}' \
--output json > iam-findings-$(date +%Y%m%d).json
GitHub organization access audit script
Audit GitHub organization members and their roles programmatically:
const { Octokit } = require("@octokit/rest");
async function auditGitHubOrg(orgName, token) {
const octokit = new Octokit({ auth: token });
const findings = [];
// Get all organization members
const members = await octokit.paginate(
octokit.rest.orgs.listMembers,
{ org: orgName, per_page: 100 }
);
for (const member of members) {
// Check if 2FA is enabled
const { data: mfaStatus } = await octokit.rest.orgs.getMembershipForUser({
org: orgName,
username: member.login,
});
// Get user's public keys to verify SSH key rotation
let keyCount = 0;
try {
const { data: keys } = await octokit.rest.users.listPublicKeysForUser({
username: member.login,
});
keyCount = keys.length;
} catch (e) {
// API may rate limit
}
findings.push({
login: member.login,
role: mfaStatus.role,
two_factor: member.two_factor_authentication ? "enabled" : "disabled",
public_keys: keyCount,
});
}
// Flag users without 2FA
const noMfa = findings.filter(f => f.two_factor === "disabled");
if (noMfa.length > 0) {
console.log(`\nUSERS WITHOUT 2FA (${noMfa.length}):`);
noMfa.forEach(u => console.log(` - ${u.login} (role: ${u.role})`));
}
// Flag admins
const admins = findings.filter(f => f.role === "admin");
console.log(`\nORGANIZATION ADMINS (${admins.length}):`);
admins.forEach(u => console.log(` - ${u.login} (2FA: ${u.two_factor})`));
return findings;
}
auditGitHubOrg("your-org", process.env.GITHUB_TOKEN)
.then(() => console.log("\nAudit complete."))
.catch(err => console.error("Audit failed:", err.message));
Shared Account Remediation Checklist
- Inventory all shared accounts (root, admin, service)
- Identify which individuals use each shared account
- Create individual accounts for each user
- Disable shared account after migration
- Implement break-glass procedure for emergency access
- Log all break-glass usage with automatic alerts
Frequently Asked Questions
Who should certify access?
The system owner or the user's direct manager is the best reviewer. For sensitive systems, the security team or data owner may also approve.
What is an orphan account?
An orphan account is an active account no longer associated with a known user or owner, often after offboarding or team changes. These should be disabled or reclaimed.
How do we make access reviews less tedious?
Use identity governance tools that pull access data automatically, provide reviewer-friendly dashboards, and auto-revoke low-risk inactive accounts after approval.
Related Resources
RBAC Policy Template
A template for defining role-based access control policies, including roles, permissions, assignment rules, and review cadence.
DocAccess Control Review Template
A template for auditing user access rights, verifying least privilege, and documenting access decisions across systems and teams.
DocSecret Rotation Schedule Template
A template for tracking and scheduling the rotation of API keys, passwords, certificates, and other secrets across systems.
DocData Retention Policy Template
A template to define how long data is kept, when it is archived, and when it must be deleted for compliance and cost reasons.