Guide · AWS CloudTrail · Audit Logging

AWS CloudTrail for MCP Servers — Trails, Event Types, and Audit Logging

AWS CloudTrail records every API call made against your AWS account — who called what, from where, and with what result — and delivers those events to S3 within 15 minutes of the call. For MCP server operators, CloudTrail is the audit foundation: it tells you which IAM role called GetSecretValue at 3 AM, which Lambda function deleted an S3 object, and which IAM policy change happened six minutes before your GuardDuty alert fired. CloudTrail management events (create, delete, update of AWS resources) are free for the first copy per region per account. Data events (S3 GetObject/PutObject, Lambda Invoke, DynamoDB GetItem) and Insights events (anomaly detection) cost extra and must be explicitly enabled — the default trail does not capture them.

TL;DR

Create one multi-region trail with log file validation, deliver to an S3 bucket hardened with Object Lock, enable CloudWatch Logs delivery for real-time alerting, and enable Insights for the trail. Enable data events only on the specific S3 buckets and Lambda functions your MCP server touches — not account-wide, or costs spiral. For per-event SQL analytics use CloudTrail Lake. For anomaly detection details see CloudTrail Insights. For log file tamper detection see log validation and hardening. For data event deep-dive see CloudTrail data events.

Create a multi-region trail

A trail is a configuration that delivers CloudTrail events to an S3 bucket (and optionally to CloudWatch Logs and SNS). Without a trail you can see the last 90 days of management events in the CloudTrail console Event History, but you cannot query older events, export to a SIEM, or trigger automated alerts. Create one trail per account (multi-region) as the baseline; add organization-level trails when you have multiple accounts under AWS Organizations.

# Step 1: Create the S3 bucket for trail delivery
# The bucket must exist before the trail can deliver to it
aws s3api create-bucket \
  --bucket mcp-cloudtrail-logs-123456789012 \
  --region us-east-1
# For regions other than us-east-1, add:
#   --create-bucket-configuration LocationConstraint=us-west-2

# Step 2: Apply the required bucket policy
# CloudTrail's service principal must be able to write to the bucket
aws s3api put-bucket-policy \
  --bucket mcp-cloudtrail-logs-123456789012 \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AWSCloudTrailAclCheck",
        "Effect": "Allow",
        "Principal": { "Service": "cloudtrail.amazonaws.com" },
        "Action": "s3:GetBucketAcl",
        "Resource": "arn:aws:s3:::mcp-cloudtrail-logs-123456789012",
        "Condition": { "StringEquals": { "aws:SourceArn": "arn:aws:cloudtrail:us-east-1:123456789012:trail/mcp-server-trail" } }
      },
      {
        "Sid": "AWSCloudTrailWrite",
        "Effect": "Allow",
        "Principal": { "Service": "cloudtrail.amazonaws.com" },
        "Action": "s3:PutObject",
        "Resource": "arn:aws:s3:::mcp-cloudtrail-logs-123456789012/AWSLogs/123456789012/*",
        "Condition": {
          "StringEquals": {
            "s3:x-amz-acl": "bucket-owner-full-control",
            "aws:SourceArn": "arn:aws:cloudtrail:us-east-1:123456789012:trail/mcp-server-trail"
          }
        }
      }
    ]
  }'

# Step 3: Create the trail — multi-region, log file validation enabled
aws cloudtrail create-trail \
  --name mcp-server-trail \
  --s3-bucket-name mcp-cloudtrail-logs-123456789012 \
  --is-multi-region-trail \
  --enable-log-file-validation \
  --include-global-service-events \
  --tags-list Key=Service,Value=mcp-server Key=Environment,Value=production

# Step 4: Start logging (trails do not log events until explicitly started)
aws cloudtrail start-logging --name mcp-server-trail

# Verify the trail is active
aws cloudtrail get-trail-status --name mcp-server-trail \
  --query '{IsLogging:IsLogging,LatestDelivery:LatestDeliveryTime,LatestDigest:LatestDigestDeliveryTime}'

The --include-global-service-events flag captures IAM, STS, and Route 53 API calls, which are global services and would otherwise be missing from regional trails. Omitting this flag means you will not see CreateUser, AssumeRole, or ChangeResourceRecordSets events — all critical for MCP server security auditing. Enable it in the home region of the trail (typically us-east-1); it is automatically excluded from the secondary regions in a multi-region trail to avoid duplicates.

# For organizations: create an organization trail that covers all member accounts
# Must be run from the management account or a delegated CloudTrail admin account
aws cloudtrail create-trail \
  --name org-mcp-server-trail \
  --s3-bucket-name org-cloudtrail-logs-management \
  --is-multi-region-trail \
  --is-organization-trail \
  --enable-log-file-validation \
  --include-global-service-events

aws cloudtrail start-logging --name org-mcp-server-trail

Event types: management, data, and Insights

CloudTrail records three distinct event categories. Understanding the cost and coverage of each is essential before deciding what to enable for your MCP server environment:

Event typeWhat it capturesDefault on?CostMCP server relevance
Management eventsCreate/delete/modify AWS resources: CreateBucket, PutRolePolicy, CreateSecret, RunInstances, UpdateFunctionCode, etc.Yes — first copy per region freeFree (1st trail); $2.00/100k events for additional trailsAudit IAM changes, Lambda deployments, Secret Manager access, security group modifications — essential baseline
Data eventsObject-level operations: S3 GetObject/PutObject/DeleteObject, Lambda InvokeFunction, DynamoDB GetItem/PutItem, CloudTrail GetResourceNo — must explicitly enable per resource or account-wide$0.10/100k events (S3); $0.10/100k events (Lambda)Trace every tool invocation: which Lambda function was called, what S3 object was read; essential for forensics; dangerous account-wide without selector filters
Insights eventsStatistical anomalies in management event API call rates or error rates, compared to a 7-day baselineNo — must enable per trail$0.35/100k events analyzedDetect compromised MCP server credentials making unusual bursts of API calls; 15-30 minute detection latency
Network activity events (preview)API calls through VPC endpoints — captures source VPC/endpoint for calls that otherwise appear to originate from the service endpointNo$0.50/100k eventsUseful when MCP server uses interface endpoints and you need to correlate source VPC with AWS API calls
# Check what event selectors are currently configured on the trail
aws cloudtrail get-event-selectors --trail-name mcp-server-trail

# Enable management events only (read + write; most common baseline)
aws cloudtrail put-event-selectors \
  --trail-name mcp-server-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [],
      "ExcludeManagementEventSources": []
    }
  ]'

# Exclude high-volume, low-signal management events to reduce noise
# KMS GenerateDataKey is a management event (confusingly) — 10M+ calls/day for
# encrypted S3 buckets can flood CloudTrail with noise; exclude it here,
# and enable KMS data events selectively if you need it
aws cloudtrail put-event-selectors \
  --trail-name mcp-server-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [],
      "ExcludeManagementEventSources": [
        "kms.amazonaws.com",
        "rdsdata.amazonaws.com"
      ]
    }
  ]'
# Remove kms.amazonaws.com exclusion if you specifically need KMS audit trail

The ExcludeManagementEventSources list is commonly used to suppress kms.amazonaws.com and rdsdata.amazonaws.com events, which can generate millions of entries per day on active deployments (every encrypted S3 read triggers a KMS management event). Excluding KMS management events is acceptable if you enable KMS data events selectively — but note that the key management operations (CreateKey, PutKeyPolicy, ScheduleKeyDeletion) are still management events and will be suppressed too. For compliance-sensitive environments, keep KMS management events enabled and pay the extra cost.

CloudWatch Logs integration for real-time alerting

Delivering CloudTrail events to CloudWatch Logs enables metric filters and alarms — effectively turning raw API call records into security monitoring. Without this integration, CloudTrail events arrive in S3 with 5–15 minute delay and are queryable only by batch tools. CloudWatch Logs integration adds another 1–3 minutes of latency but provides near-real-time filtering and alarming.

# Step 1: Create the CloudWatch Logs log group
aws logs create-log-group \
  --log-group-name /aws/cloudtrail/mcp-server-trail
aws logs put-retention-policy \
  --log-group-name /aws/cloudtrail/mcp-server-trail \
  --retention-in-days 90

# Step 2: Create an IAM role allowing CloudTrail to write to CloudWatch Logs
aws iam create-role \
  --role-name CloudTrailCloudWatchLogsRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "cloudtrail.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam put-role-policy \
  --role-name CloudTrailCloudWatchLogsRole \
  --policy-name CloudTrailCloudWatchLogsPolicy \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/cloudtrail/mcp-server-trail:*"
    }]
  }'

# Step 3: Update the trail to deliver to CloudWatch Logs
aws cloudtrail update-trail \
  --name mcp-server-trail \
  --cloud-watch-logs-log-group-arn \
    arn:aws:logs:us-east-1:123456789012:log-group:/aws/cloudtrail/mcp-server-trail:* \
  --cloud-watch-logs-role-arn \
    arn:aws:iam::123456789012:role/CloudTrailCloudWatchLogsRole
# Create a metric filter + alarm for root account usage (CIS Benchmark 1.1)
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/mcp-server-trail \
  --filter-name RootAccountUsage \
  --filter-pattern '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }' \
  --metric-transformations \
    metricName=RootAccountUsage,metricNamespace=CloudTrailMetrics,metricValue=1

aws cloudwatch put-metric-alarm \
  --alarm-name RootAccountUsage \
  --alarm-description "Root account used — investigate immediately" \
  --metric-name RootAccountUsage \
  --namespace CloudTrailMetrics \
  --statistic Sum \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts \
  --treat-missing-data notBreaching

# Metric filter for IAM policy changes (CIS Benchmark 3.4)
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/mcp-server-trail \
  --filter-name IAMPolicyChanges \
  --filter-pattern '{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=SetDefaultPolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}' \
  --metric-transformations \
    metricName=IAMPolicyChanges,metricNamespace=CloudTrailMetrics,metricValue=1

# Metric filter for console sign-in without MFA (CIS Benchmark 3.2)
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/mcp-server-trail \
  --filter-name ConsoleSignInWithoutMFA \
  --filter-pattern '{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") && ($.responseElements.ConsoleLogin = "Success") }' \
  --metric-transformations \
    metricName=ConsoleSignInWithoutMFA,metricNamespace=CloudTrailMetrics,metricValue=1

CloudTrail event structure

Every CloudTrail event is a JSON object. Understanding the key fields lets you write precise filter patterns and Logs Insights queries without guessing field names:

{
  "eventVersion": "1.09",
  "userIdentity": {
    "type": "AssumedRole",          // Root | IAMUser | AssumedRole | AWSService | AWSAccount
    "principalId": "AROAEXAMPLE:session-name",
    "arn": "arn:aws:sts::123456789012:assumed-role/mcp-server-role/i-0abc123",
    "accountId": "123456789012",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "AROAEXAMPLE",
        "arn": "arn:aws:iam::123456789012:role/mcp-server-role",
        "accountId": "123456789012",
        "userName": "mcp-server-role"
      },
      "ec2RoleDelivery": "2.0",
      "webIdFederationData": {}
    }
  },
  "eventTime": "2026-10-09T03:14:27Z",    // UTC; use for time-range queries
  "eventSource": "secretsmanager.amazonaws.com",
  "eventName": "GetSecretValue",           // the API action called
  "awsRegion": "us-east-1",
  "sourceIPAddress": "10.0.1.45",          // IP of caller; "AWS Internal" for service-to-service
  "userAgent": "aws-sdk-nodejs/3.600.0 nodejs/20.0.0",
  "requestParameters": {
    "secretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:mcp/db-creds-AbCdEf"
  },
  "responseElements": null,    // null for read-only calls that return sensitive data
  "requestID": "a1b2c3d4-1234-1234-1234-a1b2c3d4e5f6",
  "eventID": "a1b2c3d4-5678-5678-5678-a1b2c3d4e5f6",
  "readOnly": true,
  "resources": [{
    "ARN": "arn:aws:secretsmanager:us-east-1:123456789012:secret:mcp/db-creds-AbCdEf",
    "accountId": "123456789012",
    "type": "AWS::SecretsManager::Secret"
  }],
  "eventType": "AwsApiCall",    // AwsApiCall | AwsConsoleAction | AwsServiceEvent | AwsConsoleSignIn
  "managementEvent": true,
  "recipientAccountId": "123456789012",
  "errorCode": null,            // non-null on failures: AccessDenied, NoSuchBucket, etc.
  "errorMessage": null
}
# Query the last 90 days of Event History for a specific secret (no trail needed)
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=mcp/db-creds \
  --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
  --query 'Events[].{Time:EventTime,Name:EventName,User:Username,Source:CloudTrailEvent}' \
  --output table

# Lookup by event name across all resources
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteBucket \
  --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)

# Lookup by source IP
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=SourceIPAddress,AttributeValue=1.2.3.4

# Note: lookup-events only covers 90 days and is limited to management events.
# For older data or data events, query S3 logs via Athena or use CloudTrail Lake.

CloudWatch Logs Insights queries for MCP servers

Once CloudTrail delivers to CloudWatch Logs, Logs Insights provides a near-SQL query interface with sub-minute query times on recent data. These queries are useful for incident investigation without setting up Athena or CloudTrail Lake.

# Which principals called GetSecretValue in the last 24 hours?
# Run in CloudWatch Logs Insights against /aws/cloudtrail/mcp-server-trail
fields eventTime, userIdentity.arn, requestParameters.secretId, sourceIPAddress, errorCode
| filter eventName = "GetSecretValue"
| sort eventTime desc
| limit 100

# Failed API calls by principal (access denials and throttling)
fields eventTime, eventName, userIdentity.arn, errorCode, errorMessage, sourceIPAddress
| filter ispresent(errorCode) and errorCode != ""
| stats count(*) as failCount by userIdentity.arn, errorCode
| sort failCount desc
| limit 50

# IAM changes in the last hour — who changed what
fields eventTime, eventName, userIdentity.arn, requestParameters.roleName,
       requestParameters.userName, requestParameters.policyName
| filter eventSource = "iam.amazonaws.com" and eventName like /Policy|Role|User/
| sort eventTime desc
| limit 50

# Lambda function deployments (UpdateFunctionCode) — change audit
fields eventTime, userIdentity.arn, requestParameters.functionName, awsRegion, sourceIPAddress
| filter eventName = "UpdateFunctionCode20150331v2" or eventName = "UpdateFunctionCode"
| sort eventTime desc
| limit 50

# Security group changes (firewall rule audit)
fields eventTime, eventName, userIdentity.arn,
       requestParameters.groupId, requestParameters.ipPermissions
| filter eventName like /SecurityGroup/
| sort eventTime desc
| limit 100

# Secrets Manager operations by a specific Lambda function (replace ARN)
fields eventTime, eventName, requestParameters.secretId, responseElements, errorCode
| filter userIdentity.sessionContext.sessionIssuer.arn =
    "arn:aws:iam::123456789012:role/mcp-server-lambda-role"
| filter eventSource = "secretsmanager.amazonaws.com"
| sort eventTime desc
| limit 100

Pricing reference

ChargeRateFree tierMCP server estimate
Management events — first copy per regionFreeAlways free$0/month for the baseline trail
Management events — additional trails$2.00/100,000 eventsNoneNot needed unless you have multiple trails for different teams
Data events (S3, Lambda, DynamoDB, etc.)$0.10/100,000 eventsNone10M Lambda invocations/month = $10; 100M S3 GETs/month = $100 — use advanced selectors to filter
Insights events$0.35/100,000 management events analyzedNoneRoughly 0.35× management event cost for the same traffic; most MCP server environments <$5/month
CloudWatch Logs ingestion (trail delivery)$0.50/GB ingestedFirst 5 GB/monthManagement events only: ~1-5 GB/month for moderate traffic; data events can add 10-100+ GB
S3 storage for log filesStandard S3 pricing (~$0.023/GB-month)NoneCompress with S3 Intelligent-Tiering; archive to Glacier after 1 year

For most MCP server deployments, the monthly CloudTrail bill is $0–$5 for management events plus CloudWatch Logs ingestion. Data events can push this significantly higher — a busy Lambda-based MCP server with data events enabled account-wide can easily reach $50–$200/month. Use advanced event selectors to restrict data events to specific resources to keep costs predictable.

MCP server uptime and audit completeness

CloudTrail tells you what happened historically, but it does not tell you whether your MCP server is currently reachable. An MCP server that stops responding to health checks will not generate CloudTrail events — the silence looks identical to a correctly idle server. For active uptime monitoring that detects dead MCP endpoints and alerts your on-call team, see AliveMCP — it complements CloudTrail by catching availability failures that the audit log alone cannot reveal.

The two monitoring layers work together: AliveMCP catches when the server stops responding (no heartbeat), while CloudTrail catches what happened just before it stopped (last API calls, last IAM changes, last deployment). Correlating these two signals — timestamp of last successful health check from AliveMCP against the CloudTrail timeline — is the fastest way to identify the root cause of an MCP server outage.

Failure modes reference

SymptomCauseFix
Trail shows IsLogging=true but no events delivered to S3S3 bucket policy missing or incorrectly scoped; trail stopped after bucket policy removedRe-apply the bucket policy with the exact ARN from the trail; run start-logging again; check get-trail-status for LatestDeliveryError
Events visible in Event History but not in CloudWatch LogsCloudTrail-to-CloudWatch Logs IAM role missing logs:PutLogEvents permission, or log group ARN typo (must end in :*)Check get-trail-status for LatestCloudWatchLogsDeliveryError; re-apply the role policy and verify log group ARN format
Data events for S3 objects not appearingData events not enabled; trail has only management eventsUse put-event-selectors or put-advanced-event-selectors to add S3 data resources; verify with get-event-selectors
TrailAlreadyExistsException when creating a trailA trail with that name already exists in the regionList existing trails with describe-trails; update the existing trail if it exists, or choose a different name
IAM events missing from regional trail--include-global-service-events not set; IAM is a global serviceUpdate trail with --include-global-service-events; verify the trail's home region captures IAM events (secondary regions in a multi-region trail omit global events to avoid duplicates)
Log file validation digest missing after ~1 hourTrail logging stopped after creation; start-logging not calledCall start-logging; digest files are created hourly only while logging is active