Overview
This guide covers operational security best practices for deploying Sardis in production. Follow these recommendations to harden your infrastructure, protect credentials, and maintain secure AI agent payment operations.
API Key Management
Never Commit Secrets to Git
The #1 cause of credential leaks is committing secrets to git. ALWAYS use environment variables or secrets managers.
Verify .env is in .gitignore:
grep -q "^\.env$" .gitignore || echo ".env" >> .gitignore
Check git history for leaked secrets:
git log -p | grep -E "SARDIS_API_KEY|TURNKEY_API_KEY|CIRCLE_API_KEY"
If secrets are found, rotate immediately and use BFG Repo-Cleaner to remove them from history.
Use Secrets Managers
AWS Secrets Manager (Recommended for Production):
import boto3
import json
def get_secret ( secret_name : str ) -> dict :
client = boto3.client( "secretsmanager" , region_name = "us-east-1" )
response = client.get_secret_value( SecretId = secret_name)
return json.loads(response[ "SecretString" ])
secrets = get_secret( "sardis/production/api-keys" )
settings = SardisSettings(
api_key = secrets[ "SARDIS_API_KEY" ],
turnkey_api_key = secrets[ "TURNKEY_API_KEY" ],
turnkey_api_private_key = secrets[ "TURNKEY_API_PRIVATE_KEY" ],
turnkey_organization_id = secrets[ "TURNKEY_ORGANIZATION_ID" ],
)
HashiCorp Vault :
export VAULT_ADDR = "https://vault.example.com"
export VAULT_TOKEN = "s.abc123..."
# Store secrets
vault kv put secret/sardis/api-keys \
SARDIS_API_KEY="sk_live_..." \
TURNKEY_API_KEY="..." \
TURNKEY_API_PRIVATE_KEY="..."
# Retrieve secrets
vault kv get -format=json secret/sardis/api-keys
Doppler (Developer-Friendly):
# Install Doppler CLI
brew install dopplerhq/cli/doppler
# Authenticate
doppler login
# Set secrets
doppler secrets set SARDIS_API_KEY="sk_live_..."
# Run app with injected secrets
doppler run -- python main.py
Rotate API Keys Every 90 Days
Automated Rotation Script :
import os
import hashlib
import secrets
from datetime import datetime, timezone
def rotate_api_key ( agent_id : str ) -> str :
"""Generate a new API key and update it in the secrets manager."""
# Generate new key
new_key = f "sk_live_ { secrets.token_urlsafe( 32 ) } "
key_hash = hashlib.sha256(new_key.encode()).hexdigest()
# Store in database
await db.execute(
"""
INSERT INTO api_keys (agent_id, key_hash, created_at, expires_at)
VALUES ($1, $2, $3, $4)
""" ,
agent_id,
key_hash,
datetime.now(timezone.utc),
datetime.now(timezone.utc) + timedelta( days = 90 ),
)
# Update secrets manager
update_secret( f "sardis/agents/ { agent_id } /api_key" , new_key)
# Revoke old key (with 24-hour grace period)
await db.execute(
"""
UPDATE api_keys
SET revoked_at = $1
WHERE agent_id = $2 AND key_hash != $3
""" ,
datetime.now(timezone.utc) + timedelta( hours = 24 ),
agent_id,
key_hash,
)
return new_key
Set up a cron job:
# Rotate all API keys every 90 days
0 0 1 * /3 * /usr/bin/python3 /opt/sardis/scripts/rotate_api_keys.py
Scope API Keys to Specific Agents
Generate scoped API keys:
from sardis import Sardis
sardis = Sardis( api_key = os.getenv( "SARDIS_ADMIN_API_KEY" ))
# Create agent-specific API key
api_key = await sardis.api_keys.create(
agent_id = "agent_001" ,
scopes = [ "payments:create" , "policies:read" ],
rate_limit = 100 , # 100 requests per minute
ip_allowlist = [ "203.0.113.0/24" ],
)
print ( f "API Key: { api_key.key } " )
print ( f "Scopes: { api_key.scopes } " )
Enable IP Allowlisting
Turnkey : Configure in the Turnkey Dashboard
Circle : Set IP allowlist in your Circle Developer Console
Sardis API : Use middleware to enforce IP restrictions
from fastapi import Request, HTTPException
ALLOWED_IPS = { "203.0.113.0/24" , "198.51.100.0/24" }
@app.middleware ( "http" )
async def ip_allowlist_middleware ( request : Request, call_next ):
client_ip = request.client.host
if not is_ip_allowed(client_ip, ALLOWED_IPS ):
raise HTTPException( 403 , "IP not allowlisted" )
return await call_next(request)
Policy Configuration
Start with Low Trust
Default to LOW trust for all new agents:
from sardis_v2_core import create_default_policy, TrustLevel
policy = create_default_policy(
agent_id = "agent_new_001" ,
trust_level = TrustLevel. LOW , # $50/tx, $100/day
)
Upgrade to MEDIUM/HIGH only after the agent proves reliable:
# After 30 days of good behavior
if agent.days_active > 30 and agent.policy_violations == 0 :
policy.trust_level = TrustLevel. MEDIUM
policy.limit_per_tx = Decimal( "500.00" )
policy.daily_limit.limit_amount = Decimal( "1000.00" )
await policy_store.set_policy(agent.id, policy)
Use Scope Restrictions
Restrict spending to specific categories:
from sardis_v2_core import SpendingScope
# Cloud-only agent
policy.allowed_scopes = [SpendingScope. COMPUTE ]
# Retail-only agent
policy.allowed_scopes = [SpendingScope. RETAIL ]
# No restrictions
policy.allowed_scopes = [SpendingScope. ALL ]
Allow only known-good merchants:
# Allow specific cloud providers
policy.add_merchant_allow(
merchant_id = "aws.amazon.com" ,
max_per_tx = Decimal( "1000.00" ),
)
policy.add_merchant_allow(
merchant_id = "cloud.google.com" ,
max_per_tx = Decimal( "1000.00" ),
)
# Allow specific categories
policy.add_merchant_allow(
category = "cloud" ,
max_per_tx = Decimal( "500.00" ),
)
Block high-risk categories:
policy.block_merchant_category( "gambling" )
policy.block_merchant_category( "alcohol" )
policy.block_merchant_category( "tobacco" )
policy.block_merchant_category( "adult" )
Set Up Approval Thresholds
Require human approval for large transactions:
policy.approval_threshold = Decimal( "5000.00" ) # >$5k needs approval
# Evaluation returns (True, "requires_approval")
ok, reason = await policy.evaluate(
wallet = wallet,
amount = Decimal( "10000.00" ), # >$5k
fee = Decimal( "0.01" ),
chain = "base" ,
token = TokenType. USDC ,
)
if reason == "requires_approval" :
# Route to human for sign-off
await send_approval_request(agent_id, amount, merchant_id)
Monitor Policy Denials
Alert on high denial rates:
from sardis_wallet import AuditLogger, AuditAction
audit_logger = AuditLogger()
denials = await audit_logger.query(AuditQuery(
action = AuditAction. TRANSACTION_DENIED ,
start_time = datetime.now(timezone.utc) - timedelta( hours = 1 ),
))
if len (denials) > 50 :
alert( "High policy denial rate detected" , severity = "high" )
Webhook Security
Verify HMAC Signatures
Sardis webhooks include an X-Sardis-Signature header:
import hmac
import hashlib
from fastapi import Request, HTTPException
WEBHOOK_SECRET = os.getenv( "SARDIS_WEBHOOK_SECRET" )
@app.post ( "/webhooks/sardis" )
async def sardis_webhook ( request : Request):
# Get signature from header
signature = request.headers.get( "X-Sardis-Signature" )
if not signature:
raise HTTPException( 401 , "Missing signature" )
# Compute expected signature
body = await request.body()
expected_signature = hmac.new(
WEBHOOK_SECRET .encode(),
body,
hashlib.sha256,
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(signature, expected_signature):
raise HTTPException( 401 , "Invalid signature" )
# Process webhook
payload = await request.json()
await handle_webhook(payload)
return { "status" : "ok" }
Implement Replay Protection
Check timestamp to prevent replay attacks:
@app.post ( "/webhooks/sardis" )
async def sardis_webhook ( request : Request):
payload = await request.json()
# Check timestamp (reject if >5 minutes old)
timestamp = payload.get( "timestamp" )
if not timestamp:
raise HTTPException( 400 , "Missing timestamp" )
webhook_time = datetime.fromisoformat(timestamp)
age = datetime.now(timezone.utc) - webhook_time
if age > timedelta( minutes = 5 ):
raise HTTPException( 400 , "Webhook too old (replay attack?)" )
# Verify signature...
Use Webhook Secrets Rotation
Rotate webhook secrets every 90 days:
# Generate new webhook secret
NEW_SECRET = $( openssl rand -hex 32 )
# Update in Sardis dashboard
curl -X POST https://api.sardis.sh/v2/webhooks/secrets \
-H "Authorization: Bearer $SARDIS_API_KEY " \
-d '{"secret": "' $NEW_SECRET '", "rotate_at": "2026-06-01T00:00:00Z"}'
# Update in your secrets manager
aws secretsmanager update-secret \
--secret-id sardis/webhook-secret \
--secret-string " $NEW_SECRET "
Monitoring and Alerts
Set Up CloudWatch Alarms (AWS)
import boto3
cloudwatch = boto3.client( "cloudwatch" )
# Alert on high policy denial rate
cloudwatch.put_metric_alarm(
AlarmName = "SardisPolicyDenialRateHigh" ,
MetricName = "PolicyDenials" ,
Namespace = "Sardis" ,
Statistic = "Sum" ,
Period = 300 , # 5 minutes
EvaluationPeriods = 1 ,
Threshold = 50 ,
ComparisonOperator = "GreaterThanThreshold" ,
AlarmActions = [ "arn:aws:sns:us-east-1:123456789012:sardis-alerts" ],
)
# Alert on failed MPC signing
cloudwatch.put_metric_alarm(
AlarmName = "SardisMPCSigningFailures" ,
MetricName = "MPCSigningErrors" ,
Namespace = "Sardis" ,
Statistic = "Sum" ,
Period = 60 ,
EvaluationPeriods = 1 ,
Threshold = 5 ,
ComparisonOperator = "GreaterThanThreshold" ,
AlarmActions = [ "arn:aws:sns:us-east-1:123456789012:sardis-critical" ],
)
Set Up Datadog Monitors
from datadog_api_client import ApiClient, Configuration
from datadog_api_client.v1.api.monitors_api import MonitorsApi
from datadog_api_client.v1.model.monitor import Monitor
configuration = Configuration()
with ApiClient(configuration) as api_client:
api_instance = MonitorsApi(api_client)
# Alert on high API error rate
monitor = Monitor(
name = "Sardis API Error Rate" ,
type = "metric alert" ,
query = "avg(last_5m):sum:sardis.api.errors {env:production} > 50" ,
message = "Sardis API error rate is high @slack-sardis-alerts" ,
)
api_instance.create_monitor( body = monitor)
Log Critical Events
Always log these events:
from sardis_wallet import AuditLogger, AuditCategory, AuditAction, AuditLevel
audit_logger = AuditLogger()
# Key rotation
await audit_logger.log(
wallet_id = wallet.wallet_id,
category = AuditCategory. KEY_MANAGEMENT ,
action = AuditAction. KEY_ROTATED ,
level = AuditLevel. SECURITY ,
actor_id = "system" ,
details = { "reason" : "scheduled" },
)
# Policy violation
await audit_logger.log(
wallet_id = wallet.wallet_id,
category = AuditCategory. POLICY ,
action = AuditAction. TRANSACTION_DENIED ,
level = AuditLevel. WARNING ,
actor_id = agent_id,
details = { "reason" : "per_transaction_limit" , "amount" : str (amount)},
)
# Security event
await audit_logger.log_security_event(
wallet_id = wallet.wallet_id,
action = AuditAction. KEY_ROTATED ,
threat_level = "high" ,
actor_id = "security_team" ,
details = { "emergency" : True , "reason" : "potential_compromise" },
)
Set Up Alerting Rules
# Alert on policy denial rate >50%
if policy_denial_rate > 0.5 :
alert(
title = "High Policy Denial Rate" ,
severity = "high" ,
message = f "Agent { agent_id } has { policy_denial_rate :.1%} denial rate" ,
)
# Alert on velocity limit hits
if velocity_limit_hits > 5 :
alert(
title = "Agent Hitting Velocity Limits" ,
severity = "medium" ,
message = f "Agent { agent_id } hit velocity limit { velocity_limit_hits } times in 1 hour" ,
)
# Alert on failed authentication
if failed_auth_attempts > 10 :
alert(
title = "Potential Credential Stuffing Attack" ,
severity = "critical" ,
message = f " { failed_auth_attempts } failed auth attempts in 1 minute" ,
)
# Alert on unusual MPC signing activity
if signing_requests > 100 :
alert(
title = "Unusual MPC Signing Activity" ,
severity = "critical" ,
message = f " { signing_requests } signing requests per minute (normal: <10)" ,
)
Database Security
Enable Encryption at Rest
Neon (Postgres) : Enabled by default
AWS RDS :
aws rds modify-db-instance \
--db-instance-identifier sardis-production \
--storage-encrypted \
--kms-key-id arn:aws:kms:us-east-1:123456789012:key/abc123... \
--apply-immediately
Restrict Database Access
Security group rules (AWS) :
aws ec2 authorize-security-group-ingress \
--group-id sg-abc123 \
--protocol tcp \
--port 5432 \
--source-group sg-def456 # Only allow from app servers
Neon IP Allowlist :
Configure in Neon Console → Project Settings → IP Allow
Use Connection Pooling
from asyncpg import create_pool
pool = await create_pool(
dsn = os.getenv( "DATABASE_URL" ),
min_size = 10 ,
max_size = 50 ,
max_inactive_connection_lifetime = 300 ,
)
Infrastructure Hardening
Deploy a Web Application Firewall (WAF)
AWS WAF (Recommended):
aws wafv2 create-web-acl \
--name sardis-production \
--scope REGIONAL \
--default-action Block={} \
--rules file://waf-rules.json
Cloudflare (Alternative):
Enable Cloudflare in front of your API—provides DDoS protection, rate limiting, and bot detection.
Enable HTTPS with TLS 1.3
import uvicorn
uvicorn.run(
"main:app" ,
host = "0.0.0.0" ,
port = 8000 ,
ssl_keyfile = "/etc/ssl/private/sardis.key" ,
ssl_certfile = "/etc/ssl/certs/sardis.crt" ,
ssl_version = ssl. PROTOCOL_TLS_SERVER , # TLS 1.3
)
@app.middleware ( "http" )
async def add_security_headers ( request : Request, call_next ):
response = await call_next(request)
response.headers[ "X-Frame-Options" ] = "DENY"
response.headers[ "X-Content-Type-Options" ] = "nosniff"
response.headers[ "X-XSS-Protection" ] = "1; mode=block"
response.headers[ "Strict-Transport-Security" ] = "max-age=31536000; includeSubDomains"
response.headers[ "Content-Security-Policy" ] = "default-src 'self'"
return response
Enable CORS Properly
from starlette.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins = [ "https://dashboard.sardis.sh" ], # Specific origins only
allow_credentials = True ,
allow_methods = [ "GET" , "POST" , "PUT" , "DELETE" ],
allow_headers = [ "*" ],
max_age = 3600 ,
)
Incident Response
Credential Compromise
Revoke Immediately
Revoke the compromised API key in the Sardis dashboard
Rotate All Related Credentials
Rotate MPC provider credentials, database passwords, and webhook secrets
Audit Recent Activity
Query audit logs for all actions performed with the compromised key
Notify Affected Parties
If funds were lost, notify affected agents and initiate recovery
Post-Mortem
Document the incident and update security procedures
Suspicious Agent Activity
Freeze Wallet
wallet.is_frozen = True
wallet.freeze_reason = "suspicious_activity"
wallet.frozen_by = "security_team"
await wallet_manager.update_wallet(wallet)
Review Transactions
Examine all transactions in the last 24 hours for policy violations
Investigate Root Cause
Was the agent compromised? Prompt injection? Model poisoning?
Unfreeze or Terminate
If resolved, unfreeze wallet. If compromised, terminate agent.
Compliance Checklist
PCI DSS (if handling card data)
Next Steps
Threat Model Understand attack surfaces and mitigation strategies
MPC Architecture Learn how non-custodial key management works