9 Battle-Tested Non-Human Identity Security Controls
With AI services rapidly integrated into production, non-human identities (API keys, service accounts, CI tokens, and even AI agents) have become a prime target for misuse. The failure mode is consistent: keys sprawl across repos and pipelines, privileges drift, and monitoring stays human-centric. The result is quiet compromise, unexpected costs, or data access that nobody can explain.
This post gives engineering leaders a practical, code-heavy blueprint for non-human identity security in AI-first architectures – from inventory to CI/CD guardrails to runtime detection and fast revocation.

Why AI-first architectures raise machine-identity risk
AI-first systems tend to add more “machine callers” than traditional apps:
- AI agents calling internal tools (ticketing, CRM, billing, admin APIs)
- More third-party APIs (LLM, vector DB, analytics, observability, queues)
- More automation in CI/CD and platform pipelines
- More ephemeral compute (jobs, serverless, short-lived environments)
That means more API key security and service account security work, not less.
What counts as a non-human identity?
In practice, your non-human identity inventory should cover:
- API keys (vendor keys, internal API keys, webhook secrets)
- Service accounts/service principals
- IAM roles and workload identities (OIDC, STS, federation)
- CI/CD deploy tokens, runner tokens, registry tokens
- Kubernetes service accounts and secret mounts
- AI agents and bots (including “ops bots” and “LLM agents”)
The goal of non-human identity security is simple: every machine identity is discoverable, scoped, short-lived, monitored, and revocable by default.
Reference implementation (what you can ship this quarter)
A practical repo layout that supports the controls below:
.
├── identity-inventory.yaml
├── policy/
│ ├── iam.rego
│ └── secrets.rego
├── scripts/
│ ├── secret_scan.py
│ ├── validate_inventory.py
│ ├── export_cloud_identities.sh
│ ├── anomaly_detect.py
│ ├── disable_aws_key.sh
│ └── evidence_bundle.py
└── .github/workflows/
├── security-gates.yml
└── rotate-secrets.ymlControl 1) Inventory and classify API keys, service accounts, and tokens
A. Repo inventory: find leaks and stop repeats
Quick repo sweep (bash):
# Find common key patterns and obvious leaks
git grep -nE "(AKIA[0-9A-Z]{16}|xox[baprs]-|ghp_[A-Za-z0-9]{36,}|AIza[0-9A-Za-z\\-_]{35})" -- .
# Find high-entropy strings (rough heuristic)
git grep -nE "['\\\"]([A-Za-z0-9+/=]{40,})['\\\"]" -- .
# Hunt .env drift (should not exist in committed history)
git ls-files | grep -E "(^|/)\\.env(\\.|$)"Pre-commit guardrail (example .pre-commit-config.yaml):
repos:
- repo: local
hooks:
- id: block-secrets
name: Block hard-coded secrets
entry: bash -lc 'python scripts/secret_scan.py'
language: system
pass_filenames: falseMinimal Python secret scanner (entropy + pattern checks):
# scripts/secret_scan.py
import os, re, math, sys
PATTERNS = [
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key id
re.compile(r"ghp_[A-Za-z0-9]{36,}"), # GitHub token
re.compile(r"AIza[0-9A-Za-z\\-_]{35}"), # Google API key
]
def shannon_entropy(s: str) -> float:
if not s: return 0.0
freq = {c: s.count(c) / len(s) for c in set(s)}
return -sum(p * math.log2(p) for p in freq.values())
def scan_file(path: str) -> list[str]:
hits = []
try:
data = open(path, "r", encoding="utf-8", errors="ignore").read()
except Exception:
return hits
for rx in PATTERNS:
if rx.search(data):
hits.append(f"pattern:{rx.pattern}")
# entropy scan for suspicious long strings (tune thresholds per repo)
for m in re.finditer(r"['\\\"]([A-Za-z0-9+/=]{40,})['\\\"]", data):
candidate = m.group(1)
if shannon_entropy(candidate) >= 4.2:
hits.append("entropy>=4.2")
return hits
def main():
bad = []
for root, _, files in os.walk("."):
if any(seg in root for seg in [".git", "node_modules", "dist", "build", ".venv"]):
continue
for f in files:
if f.endswith((".png", ".jpg", ".zip", ".pdf")):
continue
path = os.path.join(root, f)
hits = scan_file(path)
if hits:
bad.append((path, hits))
if bad:
print("Secret scan failed. Findings:")
for path, hits in bad:
print(f"- {path}: {', '.join(hits)}")
sys.exit(1)
print("Secret scan passed.")
sys.exit(0)
if __name__ == "__main__":
main()B. Cloud inventory: export identities and last-used signals
A simple export script (extend per cloud):
# scripts/export_cloud_identities.sh
set -euo pipefail
mkdir -p evidence
echo "Exporting AWS IAM users and access keys..."
aws iam list-users --query "Users[].UserName" --output text > evidence/aws_users.txt
while read -r u; do
aws iam list-access-keys --user-name "$u" --output json > "evidence/aws_keys_${u}.json" || true
done < evidence/aws_users.txt
echo "Exporting GCP service accounts..."
gcloud iam service-accounts list --format=json > evidence/gcp_service_accounts.json || true
echo "Exporting Azure service principals..."
az ad sp list --all -o json > evidence/azure_service_principals.json || true
echo "Done. Evidence written to ./evidence/"C. Classify identities in a manifest engineers can own
Put this in version control (no secrets inside – just metadata):
# identity-inventory.yaml
version: 1
non_human_identities:
- name: payments-api-prod
type: workload_identity
platform: kubernetes
owner_team: payments
environment: prod
allowed_actions:
- read:secrets/payments/prod/*
- write:metrics/*
rotation_policy:
ttl_minutes: 60
rotation_max_days: 7
detection_baseline:
allowed_ips: ["10.0.0.0/8"]
allowed_regions: ["us-east-1"]
max_calls_per_minute: 200
- name: agent-support-bot
type: agent_identity
platform: internal
owner_team: support-platform
environment: prod
allowed_actions:
- call:/api/tickets/*:read
- call:/api/users/*:read
rotation_policy:
ttl_minutes: 15
rotation_max_days: 1
detection_baseline:
max_calls_per_minute: 60This manifest becomes the backbone of your non-human identity security program: owner, scope, TTL, and detection baseline are all reviewable.
Control 2) Replace static keys with short-lived identity by default
Static keys are hard to monitor, easy to copy, and rarely rotated fast enough. Prefer short-lived credentials:
- OIDC / federation for CI/CD and workloads
- STS-style role assumption with tight conditions
- Time-bound tokens for internal APIs (JWT with short exp)
A. GitHub Actions -> Cloud role via OIDC (no static cloud keys)
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [ "main" ]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy (OIDC)
run: ./scripts/deploy.shB. Example OIDC trust policy concept (tight repo + branch binding)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main" }
}
}
]
}C. Internal API tokens: short-exp JWT (Node.js example)
import jwt from "jsonwebtoken";
export function mintServiceToken(serviceName) {
return jwt.sign(
{ sub: serviceName, scope: ["tickets:read"], typ: "m2m" },
process.env.SIGNING_KEY,
{ expiresIn: "10m", issuer: "auth.internal" }
);
}Control 3) Least privilege by design: roles, scopes, and expiration strategies
Least privilege for non-human identities should be boring:
- No wildcards unless a compensating control exists
- Separate identities per environment (dev/stage/prod)
- Separate identities per workload (API, worker, ETL, agent)
- Explicit denies for “should never happen” actions
A. Example policy: read only one secret path, deny enumeration
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlySpecificSecrets",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": ["arn:aws:secretsmanager:us-east-1:123456789012:secret:payments/prod/*"]
},
{
"Sid": "DenyListSecrets",
"Effect": "Deny",
"Action": ["secretsmanager:ListSecrets"],
"Resource": "*"
}
]
}B. Kubernetes service account to workload identity (example annotation pattern)
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-api
namespace: payments
annotations:
# Bind this K8s SA to a cloud role/service account (provider-specific)
iam.example.com/role: payments-api-prodControl 4) CI/CD guardrails: fail builds on key misuse and dangerous IAM
Your pipeline is the cheapest place to stop bad secrets and bad identity design.
A. Secret scanning + manifest validation + policy-as-code gate (GitHub Actions)
# .github/workflows/security-gates.yml
name: security-gates
on:
pull_request:
jobs:
gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run secret scan
run: python scripts/secret_scan.py
- name: Validate identity inventory
run: python scripts/validate_inventory.py
- name: Policy-as-code for IaC
run: |
conftest test infra/ -p policy/Manifest validator (example):
# scripts/validate_inventory.py
import sys, yaml
doc = yaml.safe_load(open("identity-inventory.yaml"))
items = doc.get("non_human_identities", [])
required = ["name","type","owner_team","environment","rotation_policy"]
bad = []
for i in items:
missing = [k for k in required if k not in i]
ttl = (i.get("rotation_policy") or {}).get("ttl_minutes")
if missing:
bad.append((i.get("name","<unnamed>"), f"missing:{missing}"))
if ttl is None or int(ttl) > 120:
bad.append((i.get("name","<unnamed>"), "ttl_minutes must be <= 120"))
if bad:
print("Inventory validation failed:")
for name, reason in bad:
print(f"- {name}: {reason}")
sys.exit(1)
print("Inventory validation passed.")B. Policy-as-code: block static keys and wildcard permissions (Rego)
# policy/iam.rego
package iam
deny[msg] {
input.resource_type == "aws_iam_access_key"
msg := "Static IAM access keys are banned. Use OIDC/workload identity."
}
deny[msg] {
some s
s := input.iam_statement[_]
s.Effect == "Allow"
s.Action[_] == "*"
msg := "Wildcard Action is not allowed for non-human identities."
}C. GitLab CI version (same idea, different pipeline)
stages: [test]
security_gates:
stage: test
image: python:3.12-slim
script:
- python scripts/secret_scan.py
- python scripts/validate_inventory.pyControl 5) Runtime telemetry: alert on anomalous machine-identity activity
Good non-human identity security has runtime signals. You want alerts for:
- A key used from a new region / IP / network segment
- A service account calling a new API set
- Token usage spikes (possible looping or abuse)
- “Should never happen” actions (ListSecrets, CreateUser, wildcard assume role)
A. Emit app-side “machine identity” logs (structured)
{
"event": "non_human_identity_call",
"identity": "payments-api-prod",
"action": "secrets.read",
"resource": "secrets/payments/prod/db_password",
"request_id": "7c0e...",
"result": "success"
}B. Event rule concept: match sensitive secret reads/writes
{
"source": ["aws.secretsmanager"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["GetSecretValue", "PutSecretValue", "UpdateSecret"]
}
}C. Simple anomaly detector (Python example)
# scripts/anomaly_detect.py
import json, sys
from collections import Counter
events = [json.loads(line) for line in sys.stdin if line.strip()]
by_identity = {}
for e in events:
by_identity.setdefault(e["identity"], []).append(e)
alerts = []
for ident, evs in by_identity.items():
actions = [e["action"] for e in evs]
c = Counter(actions)
if len(c.keys()) > 20:
alerts.append((ident, "high action diversity"))
if len(evs) > 5000:
alerts.append((ident, "high call volume"))
for a in alerts:
print(f"ALERT: {a[0]}: {a[1]}")Control 6) Incident handling: short rotation + revocation playbooks
When a key leaks, you need muscle memory. Keep the playbook short:
- Identify the identity (from inventory) and blast radius (where used)
- Revoke the credential (disable key / revoke token / rotate secret)
- Replace with short-lived identity if possible (OIDC/workload identity)
- Prove closure (pipeline checks + telemetry back to baseline)
AWS key disable script (example):
# scripts/disable_aws_key.sh
set -euo pipefail
USER="$1"
KEY_ID="$2"
aws iam update-access-key --user-name "$USER" --access-key-id "$KEY_ID" --status Inactive
echo "Disabled access key $KEY_ID for user $USER"Rotation pipeline stub (example):
# .github/workflows/rotate-secrets.yml
name: rotate-secrets
on:
schedule:
- cron: "0 2 * * 1" # weekly
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rotate secrets
run: ./scripts/rotate_secrets.sh
- name: Smoke test workloads
run: ./scripts/smoke_test.shControl 7) Bridge to compliance and create audit evidence automatically
Generate evidence artifacts automatically:
- Inventory manifest (who owns each non-human identity)
- Rotation logs (when rotated, by pipeline run)
- Access logs (when used, from where, to do what)
- Exceptions (approved, time-bounded, reviewed)
Evidence bundle generator (example):
# scripts/evidence_bundle.py
import json, time, hashlib, yaml
inv = yaml.safe_load(open("identity-inventory.yaml"))
payload = {
"generated_at": int(time.time()),
"inventory_count": len(inv.get("non_human_identities", [])),
"inventory": inv.get("non_human_identities", []),
}
raw = json.dumps(payload, sort_keys=True).encode()
payload["sha256"] = hashlib.sha256(raw).hexdigest()
open("evidence/non_human_identity_evidence.json", "w").write(json.dumps(payload, indent=2))
print("Wrote evidence/non_human_identity_evidence.json")Control 8) Developer-friendly workflows and policy snippets
CODEOWNERS for identity manifests and policy:
# CODEOWNERS
/identity-inventory.yaml @platform-team @security-champions
/policy/ @platform-team @security-champions
/.github/workflows/ @platform-teamPR template snippet:
## Non-human identity security checklist (PR)
- [ ] No secrets committed (CI secret scan passes)
- [ ] Identity inventory updated (owner, TTL, baseline)
- [ ] Permissions are least privilege (no wildcards)
- [ ] Telemetry exists for identity actionsControl 9) Quick external reality check: scan your public exposure
Even strong internal non-human identity security needs an outside-in view: misconfigured endpoints, missing headers, exposed admin paths, or API surfaces can increase the impact of any credential misuse.
Use our free tool here: https://free.pentesttesting.com/
Free Website Vulnerability Scanner tools dashboard

Sample assessment report to check Website Vulnerability

Call to action
Build stronger developer security practices with real examples and expert help:
- For broader risk assessment and remediation support: https://www.pentesttesting.com/risk-assessment-services/ and https://www.pentesttesting.com/remediation-services/
- Try a no-cost evaluation: https://free.pentesttesting.com/
Where Cyber Rely can help engineering teams ship this
- Cyber Rely cybersecurity services: https://www.cybersrely.com/cybersecurity-services/
- API penetration testing services: https://www.cybersrely.com/api-penetration-testing-services/
- Web application penetration testing: https://www.cybersrely.com/web-application-penetration-testing/
Recommended next reads (recent Cyber Rely posts)
- AI Phishing Prevention: 7 Proven Dev Controls: https://www.cybersrely.com/ai-phishing-prevention-dev-controls/
- Fix a Leaked GitHub Token with 7 Powerful Steps: https://www.cybersrely.com/fix-a-leaked-github-token/
- 6 Powerful Security Chaos Experiments for CI/CD: https://www.cybersrely.com/security-chaos-experiments-for-ci-cd/
- 7 Proven Secrets as Code Patterns Engineers Need: https://www.cybersrely.com/secrets-as-code-patterns/
- 5 Proven Ways to Use LLM Pentest Agents in CI Safely: https://www.cybersrely.com/use-llm-pentest-agents-in-ci-safely/
- 10 Essential Steps: Developer Playbook for DORA: https://www.cybersrely.com/developer-playbook-for-dora/
See more: https://www.cybersrely.com/blog/
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about Non-Human Identity Security.