March 18, 2026
Security Automation

OpenClaw Credential Management: API Keys, Secrets, and Rotation Best Practices

Your agent needs to call external APIs. It needs to authenticate to GitHub, Slack, cloud providers, internal services. So you create an API key, drop it into a .env file, and move on. Congratulations—you've just created a security time bomb.

Here's the thing nobody wants to admit: most teams manage credentials badly. Not maliciously badly, just... practically badly. Keys end up in Markdown files, hardcoded in scripts, pasted into Slack during debugging, committed to git history. One leaked file, one junior engineer mistake, one social engineering attack, and your blast radius explodes overnight.

If you're running OpenClaw, you're probably generating logs, commit messages, error traces, documentation—all places where credentials love to hide. Let's talk about how to stop that from destroying you.

Table of Contents
  1. The Reality: Credentials Are Already in Your Plaintext
  2. Setup
  3. Understanding the Threat Model
  4. The Philosophy: Credentials as Ticking Clocks
  5. Strategy 1: Short-Lived Keys and Aggressive Rotation
  6. Generate Keys with Limited Lifespan
  7. Implement Automatic Rotation
  8. Credential File Locations and Organization
  9. Provider-Specific Rotation Procedures
  10. Implement Key Revocation Procedures
  11. Strategy 2: File Permissions and Encryption at Rest
  12. Restrict File Permissions Aggressively
  13. Encrypt Credentials at Rest
  14. Strategy 3: Environment Variables and Secret Management Services
  15. Inject Credentials via Environment (Not Hardcoded)
  16. Use Dedicated Secret Management Services
  17. Strategy 4: Blast Radius Analysis
  18. Map Credential Scope and Permissions
  19. Implement Least-Privilege Credentials
  20. Alert on Suspicious Usage Patterns
  21. Practical Rotation Schedule
  22. The Importance of Audit Logging
  23. The Real Cost of Credential Mismanagement
  24. Why Automation of Rotation is Non-Negotiable
  25. Decision Framework: How Aggressive Should Rotation Be?
  26. Common Mistakes in Credential Management
  27. The Long Game: Building a Culture of Credential Hygiene
  28. The Boring, Unsexy Reality

The Reality: Credentials Are Already in Your Plaintext

Let's be honest about where API keys actually live in real OpenClaw deployments. This isn't speculation. This is what we see in actual systems, over and over.

Location 1: Configuration Files

markdown
# README.md (definitely not a great idea)
 
## Setup
 
1. Get your API key from https://api.example.com
2. Set OPENAI_API_KEY=sk-proj-abc123...
3. Run the agent
 
Here's mine for reference: sk-proj-myactualkeyx9z8y7w6v5u4

This happens. Documentation writers copy their own keys as examples. Git history captures them forever. And here's the thing: even after the key is rotated, it's still in the git history. Anyone with repository access can recover it. You'd need to rewrite the entire history (which breaks everyone's checkouts) to fully remove it.

The damage doesn't stop when you notice the leak. The damage begins when you check it in. Everything after that is damage control, which is why prevention is so much better than recovery.

Location 2: .env and Config JSON

json
{
  "github_token": "ghp_abcd1234efgh5678ijkl9012mnop",
  "slack_webhook": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
  "database_password": "SuperSecure123!",
  "aws_access_key": "AKIAIOSFODNN7EXAMPLE",
  "aws_secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

These files feel "safe" because they're not in version control (usually). But they're still plaintext on disk. One misconfigured backup, one stolen laptop, one compromised container image, and someone has production access. We're not talking about sophisticated attacks. We're talking about a developer's laptop getting stolen, or a cloud backup being accidentally made public.

Consider the attack surface: backups to cloud storage (accidentally made public), developer workstations (stolen or compromised), container registries with lax permissions, CI/CD artifacts, log aggregation systems. Each of these is a potential leak point.

Location 3: Logs and Error Messages

[2026-03-17 14:32:00] ERROR: Failed to authenticate
Parameters: token=sk-proj-xyz789abc, user_id=12345, endpoint=/api/v1
Stack trace:
  at authenticate_client (line 42, client.py)
  Credentials: sk-proj-xyz789abc

Your logging system just leaked your credentials to everyone with log access. Cloud platforms, SIEMs, centralized logging—all of them might capture this. And here's what makes this insidious: the error happens once, in one log line, and then it's aggregated to a system you don't own, accessible to people you didn't explicitly authorize, kept for weeks or months.

The logging problem is especially dangerous because it's often invisible. Your code isn't intentionally logging credentials. The code logs parameters, which happen to include credentials. Then a library logs the request before it's sanitized. Then an error handler logs context. Each layer adds a little more information, and suddenly the full authentication token is exposed across multiple log lines, weeks of log history, multiple systems.

Location 4: Environment Variables in Containers

dockerfile
FROM python:3.11
ENV OPENAI_API_KEY=sk-proj-hardcoded-key
ENV DATABASE_PASSWORD=prod_password_123
RUN pip install openai

The final image contains plaintext credentials. Anyone with docker pull gets them. Container registries have been compromised. Image layers get extracted. But beyond that: environment variables set at build time are baked into the image layers. Inspect the image with docker history or docker inspect and the ENV lines are visible. This is attack surface that lasts forever—every copy of the image, every run of the image, the credentials are there.

This isn't fearmongering. These are real patterns in real systems. The first defense is admitting: your credentials are probably visible somewhere you don't want them.

Understanding the Threat Model

Before we fix this, let's be clear about what we're defending against. Credential leaks aren't hypothetical. They happen constantly. Here are the real threats:

Accidental Exposure: A developer commits to the wrong branch. A build log is accidentally made public. A test file with example credentials gets checked in. These aren't malicious; they're just mistakes. And they're common.

Infrastructure Compromise: Attackers compromise a developer's workstation, steal backups, access cloud storage. Your credentials are assets they can immediately monetize—sell them, use them for attacks, extort you with them.

Supply Chain Attacks: A dependency you use is compromised. It exfiltrates environment variables or reads .env files. Now attackers have your credentials and credentials from thousands of other companies.

Insider Threats: Disgruntled employees, contractors with access, automated systems that have more permissions than they need. Human error and malice both happen.

Third-Party Leaks: Your credentials are stored in systems owned by others—CI/CD platforms, secret management services, cloud providers. If those systems are compromised, your credentials leak. You need to assume this will happen and make it manageable.

The severity of each threat depends on what the credentials can do. If an API key is stolen, what damage can an attacker cause? That determines how aggressively you need to rotate, how strictly you need to audit usage, whether a leak is "annoying" or "catastrophic."

The Philosophy: Credentials as Ticking Clocks

Here's a mental model that helps: think of every credential as a ticking clock. The moment you create it, it has an expiration timer. The question isn't "if" it will leak, but "when" and "what damage will it cause when it does."

This changes how you approach credential management. You're not trying to prevent all leaks (impossible). You're minimizing the damage when leaks happen. You do this by:

  1. Making credentials expire fast (so the window of exposure is small)
  2. Limiting permissions (so the damage is bounded)
  3. Detecting misuse quickly (so you can respond before massive harm accrues)
  4. Making revocation immediate (so leaked credentials become useless)

This mindset is liberating. You stop expecting perfect security (which doesn't exist). Instead, you architect for graceful degradation and rapid response.

Strategy 1: Short-Lived Keys and Aggressive Rotation

If credentials do leak, you minimize damage by making them expire fast.

Generate Keys with Limited Lifespan

Most modern API providers support scoped, short-lived tokens. The principle is simple: if a key leaks, it's useless in 15 minutes instead of useless in 3 months.

GitHub example:

bash
# Generate a token valid for only 1 hour
curl -X POST https://api.github.com/authorizations \
  -u username:password \
  -d '{
    "scopes": ["repo:status"],
    "note": "OpenClaw session token",
    "expires_in": 3600
  }'

The GitHub API returns a JSON response with a token ID and the token itself. Save the token ID—you'll need it to revoke later. The token expires in 3600 seconds (1 hour). After that, the token becomes useless, even if it's been compromised and is actively being exploited.

AWS example:

bash
# Temporary credentials valid for 15 minutes
aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/OpenClawAgent \
  --role-session-name openclaw-session-1234 \
  --duration-seconds 900

This returns temporary access credentials (AccessKeyId, SecretAccessKey, SessionToken) with a specified expiration. The session token is crucial—it prevents someone from using the credentials after expiration. AWS STS (Security Token Service) is designed for exactly this: temporary credentials for specific, time-limited tasks.

Google Cloud example:

bash
# Short-lived OAuth token (1 hour default)
gcloud auth application-default print-access-token
# Token expires: 2026-03-17T15:32:00Z

Google Cloud automatically manages token expiration. Request a fresh token when needed, which is trivial. The tokens are opaque to you—you can't extend them or use them after expiration, even if you have the token string.

Anthropic/OpenAI example:

bash
# Most modern LLM APIs support standard bearer tokens
curl -X GET https://api.anthropic.com/v1/models \
  -H "Authorization: Bearer sk-ant-xyz789..." \
  -H "Content-Type: application/json"

The token is in the Authorization header. Rotate by generating a new one in your API dashboard and revoking the old.

The key principle: use the shortest lifespan that's practical for your workload. If your agent runs queries for 5 minutes at a time, use 15-minute tokens. Not 30-day or permanent.

Implement Automatic Rotation

Build rotation into your OpenClaw startup sequence. Don't rely on manual rotation. Humans forget. Systems don't.

python
class CredentialManager:
    def __init__(self, config: dict):
        self.config = config
        self.credentials = {}
        self.rotation_schedule = {}
 
    async def refresh_credentials(self):
        """Called every X minutes to rotate keys"""
        for service_name, service_config in self.config.items():
            if self._needs_rotation(service_name):
                old_key = self.credentials.get(service_name)
                new_key = await self._request_new_credential(service_config)
                self.credentials[service_name] = new_key
 
                # Log rotation (don't log actual key values)
                log_event(f"Credential rotation: {service_name} (hash: {hash_key(new_key)})")
 
                # Optionally revoke old key
                await self._revoke_credential(service_name, old_key)
 
    def _needs_rotation(self, service_name: str) -> bool:
        """Check if credential is past rotation threshold"""
        created_at = self.rotation_schedule.get(service_name)
        if not created_at:
            return True
 
        age_seconds = (datetime.now() - created_at).total_seconds()
        rotation_interval = 3600  # 1 hour
        return age_seconds > rotation_interval
 
    async def _request_new_credential(self, service_config: dict) -> str:
        """Request fresh credential from provider"""
        if service_config['type'] == 'aws':
            token = await self._aws_assume_role(service_config)
        elif service_config['type'] == 'github':
            token = await self._github_create_token(service_config)
        # ... other providers
 
        self.rotation_schedule[service_config['name']] = datetime.now()
        return token

This approach is powerful because it's automatic. Your agent doesn't ask permission to rotate. It just does, on schedule. If a key gets stolen, the window of exposure is minutes, not months. The attacker gets a credential that expires before they can even use it effectively.

Credential File Locations and Organization

Keep credentials in a single, well-organized location. Here's a recommended structure:

~/.openclaw/
├── credentials/
│   ├── github.json
│   ├── openai.json
│   ├── anthropic.json
│   ├── aws.json
│   └── slack.json
├── credentials.encrypted
├── config.yaml
└── SOUL.md

Each credential file contains the API key and metadata about when it was created and when it expires. The .encrypted file is an optional encrypted backup of all credentials.

For team setups, use a shared secret management service. For local development, keep credentials in ~/.openclaw/credentials/ and never commit them to git. Create a .gitignore entry: credentials/ to prevent accidental commits.

Provider-Specific Rotation Procedures

Different providers have different rotation workflows. Here's the detailed walkthrough for each:

GitHub Token Rotation:

The workflow is: create new token, verify it works, revoke old token, update config. This three-step process ensures you never lose access (verify before revoking), and you ensure the new token is actually functional.

  1. Generate new token via API or UI
  2. Update OpenClaw config with new token
  3. Test connectivity (try a simple API call)
  4. Revoke old token by ID
  5. Log the rotation event
python
async def rotate_github_token(self):
    """Rotate GitHub personal access token"""
    # Get current token ID (stored in metadata)
    old_token_id = self.metadata['github_token_id']
 
    # Create new token
    new_response = await self._github_create_token(
        scopes=['repo:status', 'read:repo'],
        note='OpenClaw rotation',
        expires_in=3600
    )
    new_token = new_response['token']
    new_token_id = new_response['id']
 
    # Verify it works
    test_response = await self._github_test_auth(new_token)
    if not test_response.ok:
        raise RotationError("New token failed auth test")
 
    # Update config
    self.config['github_token'] = new_token
    self.metadata['github_token_id'] = new_token_id
 
    # Revoke old
    await self._github_revoke_token(old_token_id)

AWS Credentials Rotation:

AWS is trickier because it manages multiple keys (AccessKeyId, SecretAccessKey, SessionToken). For long-term keys (access keys), rotate by creating a new pair. The strategy is: create new key, verify it works, deactivate (don't delete immediately) old key, wait for any in-flight requests to complete, then clean up.

python
async def rotate_aws_credentials(self):
    """Rotate AWS long-term credentials"""
    iam = boto3.client('iam')
 
    # Create new access key
    new_key_response = iam.create_access_key(UserName='openclaw-agent')
    new_access_key = new_key_response['AccessKey']['AccessKeyId']
    new_secret_key = new_key_response['AccessKey']['SecretAccessKey']
 
    # Verify works
    try:
        sts = boto3.client('sts',
            aws_access_key_id=new_access_key,
            aws_secret_access_key=new_secret_key
        )
        sts.get_caller_identity()  # Test call
    except Exception as e:
        iam.delete_access_key(
            UserName='openclaw-agent',
            AccessKeyId=new_access_key
        )
        raise RotationError(f"New credentials failed test: {e}")
 
    # Update config
    self.config['aws_access_key'] = new_access_key
    self.config['aws_secret_key'] = new_secret_key
 
    # Deactivate old (wait before deleting, in case rollback needed)
    old_access_key = self.config['old_aws_access_key']
    iam.update_access_key_status(
        UserName='openclaw-agent',
        AccessKeyId=old_access_key,
        Status='Inactive'
    )

OpenAI/Anthropic API Key Rotation:

Most LLM providers don't have automatic key rotation APIs yet. The process is manual: generate new key in dashboard, update config, test, revoke old. This is tedious but necessary.

python
async def rotate_openai_key(self):
    """Rotate OpenAI API key"""
    # Log into OpenAI dashboard (or use management API if available)
    # For now, manual process:
    # 1. Generate new key in dashboard at platform.openai.com/account/api-keys
    # 2. Copy new key
    # 3. Update config
 
    new_key = input("Paste new OpenAI API key: ")
 
    # Verify it works
    client = OpenAI(api_key=new_key)
    try:
        client.models.list()  # Test call
    except AuthenticationError:
        raise RotationError("New key failed authentication test")
 
    # Update config
    self.config['openai_api_key'] = new_key
 
    # Revoke old key in dashboard (manual for now)
    print("Manually revoke old key at https://platform.openai.com/account/api-keys")

Slack Webhook Rotation:

Slack webhooks are single-use URLs. Rotate by creating a new webhook and deleting the old. The process is entirely manual because Slack doesn't expose webhook management via API in a programmatic way.

python
async def rotate_slack_webhook(self):
    """Rotate Slack incoming webhook"""
    # Use Slack API to create new webhook (requires bot token)
    slack_client = WebClient(token=self.config['slack_bot_token'])
 
    response = slack_client.apps_connections_open(
        app_id=self.config['slack_app_id']
    )
    # Actually, webhooks don't have built-in rotation via API
    # Manual: Create new webhook in Slack app settings, delete old
 
    print("Manually create new webhook at Slack app settings")
    print("Then update config with new webhook URL")

Implement Key Revocation Procedures

When you rotate a credential, explicitly revoke the old one. Revocation is crucial because it prevents old credentials from being used even if they're still sitting in a leak somewhere.

python
async def revoke_github_token(self, token_id: str):
    """Revoke a GitHub PAT immediately"""
    async with aiohttp.ClientSession() as session:
        async with session.delete(
            f"https://api.github.com/authorizations/{token_id}",
            auth=aiohttp.BasicAuth(username, password)
        ) as resp:
            if resp.status == 204:
                log_event(f"Revoked GitHub token {token_id}")
            else:
                alert(f"Failed to revoke token {token_id}: {resp.status}")

Don't just replace keys. Actively revoke the old ones so stolen credentials become useless.

For AWS, deactivate immediately but wait 24 hours before deleting (in case you need to rollback). For GitHub, revoke immediately. For API keys without revocation (like some static tokens), just stop using them and note the old key as deprecated.

Strategy 2: File Permissions and Encryption at Rest

Even if you're storing credentials securely in a config file, that file itself needs protection.

Restrict File Permissions Aggressively

On Unix/Linux systems, file permissions are your first line of defense:

bash
# Config file with secrets: readable only by agent user
touch config.json
chmod 600 config.json  # rw------- (agent user only)
chown openclaw:openclaw config.json
 
# Verify
ls -la config.json
# -rw------- 1 openclaw openclaw 2048 Mar 17 14:32 config.json
 
# Never do this:
chmod 644 config.json  # WRONG: world-readable
chmod 755 config.json  # WRONG: executable world access

The principle: only the user running OpenClaw should be able to read credential files. Not "group", not "other", nobody. This is enforced by the operating system. Even if an attacker compromises another user account, they can't read your credentials (unless they escalate to root, but that's a different threat level).

For containers, this is trickier but still possible:

dockerfile
FROM python:3.11
 
# Create unprivileged user
RUN useradd -m -u 1000 openclaw
 
# Copy config
COPY config.json /app/config.json
 
# Restrict permissions
RUN chown openclaw:openclaw /app/config.json && \
    chmod 600 /app/config.json
 
# Run as unprivileged user (not root)
USER openclaw
CMD ["python", "-m", "openclaw.agent"]

Running the container as a non-root user is crucial. If the container process is compromised, the attacker only gets the permissions of that user, not root. And even within that user, file permissions ensure only the necessary files are readable.

Encrypt Credentials at Rest

Plaintext on disk is indefensible. Encrypt config files using the system's encryption capabilities or a purpose-built library. Encryption at rest means that even if someone steals the hard drive, the credentials are useless without the encryption key.

Using cryptography library:

python
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
import base64
import json
 
class EncryptedConfig:
    def __init__(self, config_path: str, password: str):
        self.config_path = config_path
        self.password = password
        self._derive_key()
 
    def _derive_key(self):
        """Derive encryption key from password"""
        salt = b'openclaw-salt'  # In production: use random salt from file header
        kdf = PBKDF2(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(self.password.encode()))
        self.cipher = Fernet(key)
 
    def load(self) -> dict:
        """Load and decrypt config file"""
        with open(self.config_path, 'rb') as f:
            encrypted_data = f.read()
        decrypted = self.cipher.decrypt(encrypted_data)
        return json.loads(decrypted)
 
    def save(self, config: dict):
        """Encrypt and save config file"""
        plaintext = json.dumps(config).encode()
        encrypted = self.cipher.encrypt(plaintext)
        with open(self.config_path, 'wb') as f:
            f.write(encrypted)
        os.chmod(self.config_path, 0o600)
 
# Usage
config = EncryptedConfig('/app/config.json', password=os.getenv('CONFIG_PASSWORD'))
credentials = config.load()
api_key = credentials['github_token']

This approach uses PBKDF2 to derive a strong encryption key from a password. The password itself needs to be managed securely (environment variable, not hardcoded). The config file is encrypted with Fernet, which provides authenticated encryption (encryption + integrity verification).

Using AWS KMS (for cloud deployments):

python
import boto3
import json
 
class KMSEncryptedConfig:
    def __init__(self, config_path: str, kms_key_id: str):
        self.config_path = config_path
        self.kms_key_id = kms_key_id
        self.kms = boto3.client('kms')
 
    def load(self) -> dict:
        """Load from encrypted file in S3 or local"""
        with open(self.config_path, 'rb') as f:
            encrypted_data = f.read()
 
        response = self.kms.decrypt(CiphertextBlob=encrypted_data)
        decrypted = response['Plaintext'].decode()
        return json.loads(decrypted)
 
    def save(self, config: dict):
        """Encrypt using KMS"""
        plaintext = json.dumps(config)
        response = self.kms.encrypt(
            KeyId=self.kms_key_id,
            Plaintext=plaintext
        )
        with open(self.config_path, 'wb') as f:
            f.write(response['CiphertextBlob'])

This approach uses AWS Key Management Service, which manages keys separately from data. The KMS key is stored in AWS's HSM (Hardware Security Module), which is much harder to compromise than a key stored with the encrypted data.

This way, even if someone gets the config file, they can't read it without the encryption key (which should be protected separately, often in a key management service).

Strategy 3: Environment Variables and Secret Management Services

Sometimes you can't store credentials on disk at all. Use environment-based management instead. Environment variables are inherited by child processes but don't persist to disk in the same way.

Inject Credentials via Environment (Not Hardcoded)

Never hardcode API keys. Always read from environment:

python
import os
 
class OpenClawConfig:
    def __init__(self):
        # These come from environment, not code
        self.github_token = os.getenv('GITHUB_TOKEN')
        self.slack_webhook = os.getenv('SLACK_WEBHOOK')
        self.openai_key = os.getenv('OPENAI_API_KEY')
 
        # Validate they're present
        if not self.github_token:
            raise ValueError("GITHUB_TOKEN environment variable not set")
 
    def validate(self):
        """Ensure no credentials in logs"""
        # Never print credential values
        print(f"Using GitHub token (first 8 chars: {self.github_token[:8]}...)")

Environment variables are injected at runtime, not baked into code or images. This matters because code is easier to inspect (and leak) than environment at runtime. When credentials come from the environment, they're not part of the source code, not part of built artifacts, not part of documentation.

Use Dedicated Secret Management Services

For production OpenClaw deployments, use a secret vault. These services provide:

  • Encrypted storage
  • Access audit logs
  • Automatic rotation triggers
  • Revocation capabilities
  • Separation of duties

Kubernetes Secrets:

yaml
apiVersion: v1
kind: Secret
metadata:
  name: openclaw-credentials
type: Opaque
stringData:
  github-token: ghp_abcd1234...
  slack-webhook: https://hooks.slack.com/...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openclaw-agent
spec:
  template:
    spec:
      containers:
        - name: agent
          env:
            - name: GITHUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: openclaw-credentials
                  key: github-token

Kubernetes Secrets are base64-encoded by default (not encrypted), so use encryption at rest in your cluster. The advantage: secrets are managed by Kubernetes, rotated with your deployments, and access-controlled by RBAC.

HashiCorp Vault:

python
import hvac
 
class VaultCredentialManager:
    def __init__(self, vault_addr: str, vault_token: str):
        self.client = hvac.Client(url=vault_addr, token=vault_token)
 
    def get_credential(self, path: str) -> str:
        """Retrieve credential from Vault"""
        secret = self.client.secrets.kv.read_secret_version(path=path)
        return secret['data']['data']['value']
 
    def rotate_credential(self, path: str, new_value: str):
        """Update credential in Vault"""
        self.client.secrets.kv.create_or_update_secret_version(
            path=path,
            secret_data={'value': new_value}
        )
 
# Usage
vault = VaultCredentialManager('https://vault.company.com', token)
github_key = vault.get_credential('openclaw/github-token')

Vault is a dedicated secret management system. Credentials are encrypted, audited, and can be rotated without redeploying your application. It's more operational overhead but gives you fine-grained control and audit trails.

Strategy 4: Blast Radius Analysis

When a credential leaks, how much damage can an attacker do? Know your exposure.

Map Credential Scope and Permissions

For each credential your agent uses, document:

yaml
Credential: github_token
Scope:
  - Repositories: openclaw, related-tools (read + write)
  - Permissions: read:repo, write:repo, delete:repo
  - User access: via API, not web UI
  - Blast radius: Can push commits, delete branches, modify issues
 
Credential: slack_webhook
Scope:
  - Channels: #openclaw-logs, #alerts
  - Permissions: Post messages only (no delete, no user impersonation)
  - User access: Webhook only, not token
  - Blast radius: Can post messages (annoying, not catastrophic)
 
Credential: database_password
Scope:
  - Database: production-analytics
  - Permissions: SELECT, INSERT on specific tables
  - User access: Direct DB access
  - Blast radius: Can exfiltrate data, corrupt tables
  - MITIGATION: IP-restricted connections, separate read-only replica for agent

Understanding blast radius is crucial. A leaked Slack webhook is annoying (someone posts to your channels). A leaked database password with full admin rights is catastrophic. This shapes your rotation frequency and your response plan.

Implement Least-Privilege Credentials

Give each credential only the permissions it actually needs:

python
# GitHub token with minimal scope
github_token_config = {
    "scopes": ["repo:status"],  # Read-only status checks
    "note": "OpenClaw code review agent",
    "expires_in": 3600,  # 1 hour
}
 
# NOT:
# admin:repo_hook (full control)
# delete_repo (can destroy repos)
# user (full user impersonation)
 
# Database user with restricted permissions
CREATE USER openclaw_agent@localhost IDENTIFIED BY 'temporary_key';
GRANT SELECT ON database.read_table TO 'openclaw_agent'@'localhost';
GRANT INSERT ON database.logs TO 'openclaw_agent'@'localhost';
# NOT: DROP, DELETE, or admin privileges

Limit each credential to exactly what the agent needs. No more. If code gets compromised, attackers are constrained by the credential's permissions. This is defense in depth: even if multiple defenses fail, you've limited what an attacker can do with stolen credentials.

Alert on Suspicious Usage Patterns

Monitor for credential misuse. Set baselines for normal usage and alert when things deviate:

python
async def monitor_credential_usage(credential_id: str, usage: dict):
    """Alert if credential is being used unexpectedly"""
    baseline = load_usage_baseline(credential_id)
 
    checks = [
        usage['requests_per_minute'] > baseline['requests_per_minute'] * 5,
        usage['locations'] != baseline['locations'],  # Used from unexpected IP
        usage['failed_auth_attempts'] > 3,
        usage['apis_accessed'] not in baseline['apis_accessed'],  # New API calls
    ]
 
    if any(checks):
        alert_security_team(f"ANOMALOUS USAGE: {credential_id}", usage)
        # Optionally revoke automatically
        revoke_credential(credential_id)

If a credential is stolen and used, catch it before massive damage accrues. Anomaly detection is your safety net: when normal behavior changes, something's wrong.

Practical Rotation Schedule

Here's a recommended rotation schedule for different credential types:

Credential TypeRotation FrequencyReasonImplementation
Short-lived tokens (AWS STS)15 minutesMinimize exposure windowAutomatic, built into agent startup
GitHub PATs1 hour (dev), 24 hours (prod)Balance between rotation overhead and securityScheduled job, event-driven
API keys (OpenAI, Anthropic)WeeklyModerate security risk, low rotation overheadScheduled job with alerts
Database passwordsMonthlyHigh value target, but rotation is complexScheduled with coordination
Slack webhooksQuarterlyLow rotation frequency, create when neededManual or event-driven
Long-term AWS keysEvery 90 daysRegulatory requirementScheduled with grace period

Start aggressive (rotate everything daily or more) and relax based on operational overhead and actual security needs.

The Importance of Audit Logging

Beyond the strategies above, you need visibility into credential usage. Audit logging answers: Who accessed what? When? From where? If a credential is compromised, your audit logs should show exactly when the anomaly started.

Logging requires care, though. Never log the credential value itself. Log the credential ID, hash, or first few characters:

python
# GOOD: Logs the action without exposing the key
logger.info(f"GitHub API call using token {token_hash}", extra={
    "token_id": "ghp_abc123...",  # First 8 chars, not the whole key
    "endpoint": "/repos/user/repo/pulls",
    "response_code": 200,
    "timestamp": datetime.now()
})
 
# BAD: Logs the full credential
logger.info(f"Using token {full_github_token} to call GitHub")

Audit logs should be centralized, immutable (can't be deleted), and retained for months or years. If you discover a compromise in week 2, you need logs from week 1 to understand the damage.

The Real Cost of Credential Mismanagement

Let's talk about what happens when you do this wrong. A team I worked with had a developer commit an AWS access key to a public repository. Within 48 hours, attackers had noticed, used the credentials to launch cryptocurrency mining operations on their EC2 instances, and racked up $47,000 in charges.

The remediation was expensive: they had to audit every action taken with those credentials, revoke and rotate everything the key had access to, shutdown compromised instances, deal with the billing dispute. The whole process took two weeks of dedicated security work.

The initial mistake—hardcoding a key—took 30 seconds. The cleanup took two weeks and cost tens of thousands of dollars. That's the math of credential management. The prevention cost is always smaller than the recovery cost.

Even worse: they didn't discover the compromise themselves. AWS eventually notified them days after the mining started. Those days of undetected activity could have involved data exfiltration, privilege escalation, or lateral movement through their infrastructure.

If they'd implemented even basic rotation (weekly) and anomaly detection (alert on unusual API activity), they would have caught it in hours, not days. The damage would have been a fraction of what it became.

Why Automation of Rotation is Non-Negotiable

Let's be clear about something: if you're manually rotating credentials, you're already behind. Manual rotation is where systems fail. Not because humans are bad. Because humans forget, get busy, deprioritize, and eventually skip rotation schedules.

I've seen teams with excellent intentions. "We'll rotate keys monthly," they promise. Month one goes fine. Month two, someone gets busy and it gets bumped. Month three, nobody remembers whose job it was. By month six, the keys haven't rotated in five months, and nobody noticed until a compliance audit or a leaked key.

Automation solves this. It's not about trust. It's about removing the human decision from the equation. The credential rotates whether you remember or not. Whether you're busy or not. Whether you think it's important today or not.

The engineering overhead to set up rotation automation is typically 2-4 hours per API provider. The payoff is decades of not having to think about it. That's a no-brainer tradeoff.

When you automate rotation, several good things happen:

  • You rotate faster (15 minutes instead of monthly) because the friction is gone
  • You catch compromises faster because the window is small
  • You eliminate entire categories of compliance violations
  • You can be confident that if a key leaks, it's not a disaster (it expires tomorrow)

This changes the psychology. When you know keys expire fast and rotation is automatic, the threat model shifts. A leaked key isn't a five-alarm fire that requires rewriting history. It's a minor inconvenience that the system handles.

Decision Framework: How Aggressive Should Rotation Be?

Here's a common question: "Should I rotate everything every day, or is that overkill?"

The answer is: it depends on your threat model and your operational capacity. But here's a framework for thinking about it:

Ask these questions for each credential:

  1. What's the worst-case damage if this key leaks? If it's "someone posts spam to a channel," rotate quarterly. If it's "someone exfiltrates our customer database," rotate daily.

  2. How easy is rotation for this provider? If it's automatic (like AWS STS), go aggressive (15 minutes). If it's manual (like most LLM providers), go conservative (weekly).

  3. What's our detection lag? If we monitor usage closely and would notice misuse within an hour, we can use longer lifespans. If we don't have good monitoring, rotate faster.

  4. What's the operational burden? If every rotation requires human intervention, you won't stick to it. Build automation first, then set the schedule.

A reasonable starting point is:

  • Automatic credentials (AWS STS, Google Cloud): 15 minutes
  • Credentials you can rotate via API (GitHub): 1 hour (dev), 24 hours (prod)
  • Credentials that require manual action (most LLM providers): Weekly
  • Database passwords (high impact, complex to rotate): Monthly

From there, you can adjust based on actual experience. You might find that weekly rotation of API keys is fine because compromises are rare. Or you might find that you have production issues once a month because someone forgot to update the key, so you automate it and rotate daily.

The key principle: be more aggressive than you think you need to be. The cost of unnecessary rotation is small. The cost of insufficient rotation is potentially catastrophic.

Common Mistakes in Credential Management

Beyond the wrong locations and missing rotation, here are pitfalls teams regularly fall into:

Mistake 1: Mixing Development and Production Credentials

You create one GitHub token and use it everywhere. Development, CI/CD, production deployments. If that token leaks, the blast radius is everything.

Better: Separate tokens per environment. Development keys can have broad permissions (faster iteration). Production keys have minimal permissions (constrained blast radius). Tokens have short lifespans that match their environment.

Mistake 2: Never Testing Rotation

You build a rotation system and never actually test it. Then it fails in production. Testing rotation is straightforward: rotate the credential manually, verify the system handles it gracefully, then automate it.

But many teams skip this. They assume the automation will work. It won't, not on first try. Test before you depend on it.

Mistake 3: Keeping Credentials Longer Than Necessary

There's a psychological comfort in not rotating often. Stable keys feel safe. But they're not. Old credentials are more likely to be exposed somewhere, just from the law of large numbers. The longer a key exists, the more chances for leaks to happen.

Short-lived credentials flip this: they're less likely to be compromised because they haven't had time to accumulate exposure.

Mistake 4: No Audit Logging

You can't know if a credential was compromised if you don't monitor its usage. Audit logging isn't optional. It's how you detect that something went wrong when it actually happens.

Minimal audit logging: log when credentials are used, who/what created them, who/what revoked them. Don't log the credentials themselves, but log the metadata.

Mistake 5: Assuming "Nobody Will Find It"

A surprising number of teams commit credentials to version control and think "Well, the repository is private, so it's fine." Private repositories get compromised, shared inappropriately, transferred to different organizations. Assume any credential checked into git will eventually be seen by someone who shouldn't see it.

Treat version control as the least secure environment and never put credentials there.

The Long Game: Building a Culture of Credential Hygiene

The real win isn't in any one tactic. It's in building a team culture where credential hygiene is default behavior. Where rotating keys feels as natural as committing code. Where someone seeing a hardcoded credential immediately flags it as wrong, the same way they'd flag bad code.

This culture takes time. But it starts with:

  1. Making rotation automatic so there's no way to skip it
  2. Making encrypted storage the default (not an afterthought)
  3. Making audit logging visible (so people see when credentials are used)
  4. Making it easy to rotate (so there's no friction or excuses)

When all of these are true, credential hygiene becomes "just how we do things." New team members see it's normal. Contractors understand the expectation. Systems enforce it. That's when you can actually sleep at night.

The Boring, Unsexy Reality

Good credential management isn't flashy. It's:

  • Short-lived keys that expire automatically
  • Encryption on disk and in transit
  • Aggressive file permissions nobody remembers to check
  • Rotation schedules that feel paranoid
  • Monitoring for anomalies that usually don't happen
  • Separation of duties that complicates onboarding

But it works. An attacker with a leaked key that expires in 15 minutes is an attacker with almost nothing. A credential with minimal permissions and immediate revocation capability is a credential that limits blast radius to acceptable levels.

The teams that sleep well at night aren't the ones with perfect security. They're the ones who know what will break if credentials leak, and built systems to make that breaking slow, detectable, and limited.

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project