PCI-DSS 4.0 Req 7.2.2 requires that privileges assigned to accounts and roles are based on least privilege. Req 8.2.1 limits account use to the single purpose for which it was created. A reporting cron job running with full database admin credentials means a compromised job scheduler has write access to the entire CDE. CWE-250 (Execution with Unnecessary Privileges) and OWASP A01 (Broken Access Control) both flag over-privileged service accounts as exploitable. NIST AC-6 (Least Privilege) mandates that each automated process is granted only the minimum rights required for its function.
Low because over-privileged service accounts expand blast radius when compromised but typically require an upstream component compromise first — the risk is real but conditional on a prior step in the attack chain.
Audit every service account, API key, and IAM role in your infrastructure and downscope each to the minimum required permissions. Create named database users per job rather than sharing an admin credential.
-- Read-only user for nightly reporting job
CREATE USER reporting_job PASSWORD 'generated_secret';
GRANT CONNECT ON DATABASE main TO reporting_job;
GRANT USAGE ON SCHEMA public TO reporting_job;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_job;
For AWS IAM, replace "Action": "*" with an explicit list of required actions and a specific resource ARN. For Stripe, create a Restricted Key in the dashboard scoped to only the operations each service needs — webhook handlers need webhooks:read, not full account access. Document each service account's scope in docs/rbac.md.
ID: ecommerce-pci.access-control.service-accounts-least-privilege
Severity: low
What to look for: Enumerate all service accounts, API keys, and automated process credentials in the project. For each one, classify its permission scope as narrow (read-only, single-resource) or broad (admin, full-access, wildcard). Count the number of service accounts with overly broad permissions. Check IAM policies for "Action": "*" or "Resource": "*" wildcards, database users with superuser/admin roles, and API keys without restricted scopes.
Pass criteria: At least 1 service account or API key exists with narrowly scoped permissions. No more than 0 service accounts have overly broad permissions (admin, wildcard, or superuser scope). Each service has only the minimum permissions required for its function — a reporting job uses read-only access, a webhook receiver uses a restricted key. Report: "X service accounts found, Y with narrow scope, Z with broad scope."
Fail criteria: At least 1 service account has overly broad permissions (database admin for a read-only job, API key with full account access for a single webhook, IAM policy with "Action": "*").
Skip (N/A) when: No service accounts, API keys, or automated processes exist (no cron jobs, no webhook handlers, no background workers, no IAM policies in infrastructure code).
Detail on fail: Identify service accounts with excessive permissions. Example: "Reporting cron job uses database admin credentials (superuser) instead of read-only user. 1 of 2 service accounts has overly broad permissions." or "IAM policy in terraform/main.tf grants Action: * on Resource: *"
Remediation: Create minimal-scope service accounts. For database:
-- Create read-only user for reporting job
CREATE USER reporting_job_user PASSWORD 'secure_random_password';
GRANT CONNECT ON DATABASE main TO reporting_job_user;
GRANT USAGE ON SCHEMA public TO reporting_job_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_job_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO reporting_job_user;
For API keys (Stripe, AWS):
// Restricted Stripe key for webhook verification only
const stripeWebhookKey = process.env.STRIPE_WEBHOOK_KEY; // read-only, webhook-only
// Full key stored separately, used only in secure backend
const stripeAdminKey = process.env.STRIPE_ADMIN_KEY; // admin key, never exposed
For AWS IAM:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/reports/*"
}
]
}