Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automate credential rotation for service accounts across Active Directory, cloud platforms, and application databases to eliminate stale secrets and reduce compromise risk.
.claude/skills/performing-service-account-credential-rotation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✓→✓ | = Same ✓ | — | — |
| case-04 | ✗→✗ | = Same ✗ | — | — |
| case-13 | ✗→✗ | = Same ✗ | — | — |
| case-06 | ✗→✗ | = Same ✗ | — | — |
Service accounts are non-human identities used by applications, daemons, CI/CD pipelines, and automated processes to authenticate to systems and APIs. These accounts often have elevated privileges and their credentials (passwords, API keys, certificates, tokens) are frequently long-lived and shared across teams, making them prime targets for attackers. Credential rotation is the systematic process of replacing these secrets on a scheduled basis, propagating new credentials to all dependent systems, and verifying service continuity after rotation.
| Type | Platform | Credential | Rotation Method | |------|----------|-----------|-----------------| | Active Directory Service Account | Windows/AD | Password | gMSA (automatic) or PAM-managed | | AWS IAM User | AWS | Access Key/Secret Key | AWS Secrets Manager rotation Lambda | | GCP Service Account | GCP | JSON key file | Key rotation via IAM API | | Azure Service Principal | Azure | Client secret/certificate | Key Vault + rotation policy | | Database Service Account | SQL/Oracle/Postgres | Password | Vault dynamic secrets | | API Key | SaaS applications | API token | Application-specific API |
Windows gMSAs provide automatic password management by Active Directory:
Secrets Manager / Vault
│
├── Rotation Trigger (schedule or on-demand)
│
├── Generate new credential
│
├── Update credential at source (AD, cloud IAM, database)
│
├── Update credential in all consumers:
│ ├── Application configuration
│ ├── CI/CD pipeline secrets
│ ├── Kubernetes secrets
│ └── Other dependent services
│
├── Verify service health
│ ├── Health check endpoints
│ ├── Authentication test
│ └── Functional smoke test
│
└── Revoke old credential (after grace period)Enumerate all service accounts and their dependencies:
powershell# Active Directory: Find all service accounts Get-ADServiceAccount -Filter * -Properties * Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName,PasswordLastSet,LastLogonDate # Find accounts with passwords older than 90 days $threshold = (Get-Date).AddDays(-90) Get-ADUser -Filter {PasswordLastSet -lt $threshold -and Enabled -eq $true} -Properties PasswordLastSet,ServicePrincipalName | Where-Object {$_.ServicePrincipalName} | Select-Object Name, PasswordLastSet, ServicePrincipalName
powershell# Create KDS Root Key (one-time, domain-wide) Add-KdsRootKey -EffectiveImmediately # Create the gMSA account New-ADServiceAccount -Name "svc-webapp-gmsa" ` -DNSHostName "svc-webapp-gmsa.corp.example.com" ` -PrincipalsAllowedToRetrieveManagedPassword "WebServerGroup" ` -KerberosEncryptionType AES128,AES256 # Install on target server Install-ADServiceAccount -Identity "svc-webapp-gmsa" # Test the account Test-ADServiceAccount -Identity "svc-webapp-gmsa" # Configure IIS Application Pool to use gMSA # Set identity to: CORP\svc-webapp-gmsa$
pythonimport boto3 import json def rotate_iam_access_key(secret_arn, iam_username): """Rotate an IAM user's access key via Secrets Manager.""" iam = boto3.client("iam") sm = boto3.client("secretsmanager") # Create new access key new_key = iam.create_access_key(UserName=iam_username) new_access_key = new_key["AccessKey"]["AccessKeyId"] new_secret_key = new_key["AccessKey"]["SecretAccessKey"] # Store new credentials in Secrets Manager sm.put_secret_value( SecretId=secret_arn, SecretString=json.dumps({ "accessKeyId": new_access_key, "secretAccessKey": new_secret_key, "username": iam_username, }) ) # List old access keys and deactivate them keys = iam.list_access_keys(UserName=iam_username) for key in keys["AccessKeyMetadata"]: if key["AccessKeyId"] != new_access_key and key["Status"] == "Active": iam.update_access_key( UserName=iam_username, AccessKeyId=key["AccessKeyId"], Status="Inactive" ) return {"new_key_id": new_access_key, "old_keys_deactivated": True}
pythonimport hvac def configure_vault_database_rotation(vault_url, vault_token, db_config): """Configure HashiCorp Vault for automatic database credential rotation.""" client = hvac.Client(url=vault_url, token=vault_token) # Enable database secrets engine client.sys.enable_secrets_engine( backend_type="database", path="database" ) # Configure database connection client.secrets.database.configure( name=db_config["name"], plugin_name="postgresql-database-plugin", connection_url=f"postgresql://{{{{username}}}}:{{{{password}}}}@" f"{db_config['host']}:{db_config['port']}/{db_config['database']}", allowed_roles=[db_config["role_name"]], username=db_config["admin_user"], password=db_config["admin_password"], ) # Create a role for dynamic credentials client.secrets.database.create_role( name=db_config["role_name"], db_name=db_config["name"], creation_statements=[ "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';", f"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{{{name}}}}\";" ], default_ttl="1h", max_ttl="24h", ) return {"status": "configured", "role": db_config["role_name"]}
After every rotation, verify service continuity:
pythonimport requests import time def verify_service_health(service_endpoints, max_retries=3, delay=10): """Check that services are healthy after credential rotation.""" results = [] for endpoint in service_endpoints: for attempt in range(max_retries): try: response = requests.get( endpoint["health_url"], timeout=10, headers=endpoint.get("headers", {}) ) healthy = response.status_code == 200 results.append({ "service": endpoint["name"], "status": "healthy" if healthy else f"unhealthy ({response.status_code})", "attempt": attempt + 1, }) if healthy: break except requests.RequestException as e: results.append({ "service": endpoint["name"], "status": f"error: {str(e)}", "attempt": attempt + 1, }) if attempt < max_retries - 1: time.sleep(delay) return results
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.