Skip to content

CloudTrail Setup

Set up AWS CloudTrail to track API calls, monitor security events, and maintain compliance audit trails for Pocketbook deployments on AWS.

Overview

AWS CloudTrail provides:

  • Audit logging: Record all API calls and actions
  • Compliance: Meet regulatory requirements (SOC 2, HIPAA, PCI-DSS)
  • Security monitoring: Detect unauthorized access
  • Incident investigation: Track events for forensic analysis
  • Governance: Monitor resource changes

Prerequisites

  • AWS Account with admin access
  • IAM permissions for CloudTrail, S3, and CloudWatch
  • S3 bucket for log storage
  • (Optional) CloudWatch Logs for real-time monitoring

Basic CloudTrail Setup

Create S3 Bucket for Logs

bash
# Create S3 bucket
aws s3api create-bucket \
  --bucket pocketbook-cloudtrail-logs \
  --region us-east-1

# Enable versioning
aws s3api put-bucket-versioning \
  --bucket pocketbook-cloudtrail-logs \
  --versioning-configuration Status=Enabled

# Block public access
aws s3api put-public-access-block \
  --bucket pocketbook-cloudtrail-logs \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Create S3 Bucket Policy

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AWSCloudTrailAclCheck",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:GetBucketAcl",
      "Resource": "arn:aws:s3:::pocketbook-cloudtrail-logs"
    },
    {
      "Sid": "AWSCloudTrailWrite",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::pocketbook-cloudtrail-logs/AWSLogs/123456789012/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": "bucket-owner-full-control"
        }
      }
    }
  ]
}

Apply policy:

bash
aws s3api put-bucket-policy \
  --bucket pocketbook-cloudtrail-logs \
  --policy file://bucket-policy.json

Create CloudTrail

bash
aws cloudtrail create-trail \
  --name pocketbook-audit-trail \
  --s3-bucket-name pocketbook-cloudtrail-logs \
  --is-multi-region-trail \
  --enable-log-file-validation

# Start logging
aws cloudtrail start-logging \
  --name pocketbook-audit-trail

CloudTrail Configuration

Enable Specific Events

Management Events

bash
aws cloudtrail put-event-selectors \
  --trail-name pocketbook-audit-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": []
    }
  ]'

Data Events (S3)

Track S3 object-level operations:

bash
aws cloudtrail put-event-selectors \
  --trail-name pocketbook-audit-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [
        {
          "Type": "AWS::S3::Object",
          "Values": [
            "arn:aws:s3:::pocketbook-uploads/*"
          ]
        }
      ]
    }
  ]'

Data Events (Lambda)

Track Lambda function invocations:

bash
aws cloudtrail put-event-selectors \
  --trail-name pocketbook-audit-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [
        {
          "Type": "AWS::Lambda::Function",
          "Values": [
            "arn:aws:lambda:us-east-1:123456789012:function/*"
          ]
        }
      ]
    }
  ]'

CloudFormation Template

yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudTrail setup for Pocketbook'

Resources:
  CloudTrailBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: pocketbook-cloudtrail-logs
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldLogs
            Status: Enabled
            ExpirationInDays: 2555  # 7 years for compliance
            Transitions:
              - TransitionInDays: 90
                StorageClass: GLACIER
              - TransitionInDays: 365
                StorageClass: DEEP_ARCHIVE

  CloudTrailBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref CloudTrailBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: 's3:GetBucketAcl'
            Resource: !GetAtt CloudTrailBucket.Arn
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: 's3:PutObject'
            Resource: !Sub '${CloudTrailBucket.Arn}/AWSLogs/${AWS::AccountId}/*'
            Condition:
              StringEquals:
                's3:x-amz-acl': 'bucket-owner-full-control'

  CloudTrail:
    Type: 'AWS::CloudTrail::Trail'
    DependsOn: CloudTrailBucketPolicy
    Properties:
      TrailName: pocketbook-audit-trail
      S3BucketName: !Ref CloudTrailBucket
      IsLogging: true
      IsMultiRegionTrail: true
      EnableLogFileValidation: true
      IncludeGlobalServiceEvents: true
      EventSelectors:
        - ReadWriteType: All
          IncludeManagementEvents: true

  CloudTrailLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: /aws/cloudtrail/pocketbook
      RetentionInDays: 90

  CloudTrailRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: CloudTrailLogPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !GetAtt CloudTrailLogGroup.Arn

Deploy:

bash
aws cloudformation create-stack \
  --stack-name pocketbook-cloudtrail \
  --template-body file://cloudtrail.yaml \
  --capabilities CAPABILITY_IAM

CloudWatch Integration

Send Logs to CloudWatch

bash
# Create CloudWatch Logs group
aws logs create-log-group \
  --log-group-name /aws/cloudtrail/pocketbook

# Create IAM role for CloudTrail
aws iam create-role \
  --role-name CloudTrailRole \
  --assume-role-policy-document file://trust-policy.json

# Attach policy
aws iam put-role-policy \
  --role-name CloudTrailRole \
  --policy-name CloudTrailLogPolicy \
  --policy-document file://log-policy.json

# Update trail
aws cloudtrail update-trail \
  --name pocketbook-audit-trail \
  --cloud-watch-logs-log-group-arn arn:aws:logs:us-east-1:123456789012:log-group:/aws/cloudtrail/pocketbook:* \
  --cloud-watch-logs-role-arn arn:aws:iam::123456789012:role/CloudTrailRole

Create CloudWatch Alarms

bash
# Alert on root account usage
aws cloudwatch put-metric-alarm \
  --alarm-name pocketbook-root-account-usage \
  --alarm-description "Alert when root account is used" \
  --metric-name RootAccountUsage \
  --namespace CloudTrailMetrics \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts

# Alert on unauthorized API calls
aws cloudwatch put-metric-alarm \
  --alarm-name pocketbook-unauthorized-api-calls \
  --alarm-description "Alert on unauthorized API calls" \
  --metric-name UnauthorizedAPICalls \
  --namespace CloudTrailMetrics \
  --statistic Sum \
  --period 300 \
  --threshold 5 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts

Metric Filters

Track Security Events

bash
# Filter pattern for failed console logins
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/pocketbook \
  --filter-name FailedConsoleLogins \
  --filter-pattern '{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }' \
  --metric-transformations \
    metricName=FailedConsoleLogins,metricNamespace=CloudTrailMetrics,metricValue=1

# Filter pattern for IAM policy changes
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/pocketbook \
  --filter-name IAMPolicyChanges \
  --filter-pattern '{ ($.eventName = PutUserPolicy) || ($.eventName = PutRolePolicy) || ($.eventName = PutGroupPolicy) }' \
  --metric-transformations \
    metricName=IAMPolicyChanges,metricNamespace=CloudTrailMetrics,metricValue=1

# Filter pattern for S3 bucket changes
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/pocketbook \
  --filter-name S3BucketChanges \
  --filter-pattern '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = DeleteBucket)) }' \
  --metric-transformations \
    metricName=S3BucketChanges,metricNamespace=CloudTrailMetrics,metricValue=1

Query CloudTrail Logs

Using AWS CLI

bash
# Look up recent events
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=RunInstances \
  --max-results 10

# Filter by user
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=admin@pocketbook.com \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-31T23:59:59Z

# Filter by resource
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=pocketbook-production

Using CloudWatch Logs Insights

sql
-- Find all failed authentication attempts
fields @timestamp, userIdentity.principalId, errorMessage
| filter eventName = "ConsoleLogin" and errorMessage = "Failed authentication"
| sort @timestamp desc
| limit 100

-- Track IAM changes
fields @timestamp, userIdentity.arn, eventName, requestParameters
| filter eventSource = "iam.amazonaws.com"
| sort @timestamp desc

-- S3 access patterns
fields @timestamp, userIdentity.principalId, eventName, requestParameters.bucketName
| filter eventSource = "s3.amazonaws.com"
| stats count() by eventName

Using Athena

Create Athena table:

sql
CREATE EXTERNAL TABLE cloudtrail_logs (
  eventversion STRING,
  useridentity STRUCT<
    type:STRING,
    principalid:STRING,
    arn:STRING,
    accountid:STRING,
    invokedby:STRING,
    accesskeyid:STRING,
    userName:STRING,
    sessioncontext:STRUCT<
      attributes:STRUCT<
        mfaauthenticated:STRING,
        creationdate:STRING>,
      sessionissuer:STRUCT<
        type:STRING,
        principalId:STRING,
        arn:STRING,
        accountId:STRING,
        userName:STRING>>>,
  eventtime STRING,
  eventsource STRING,
  eventname STRING,
  awsregion STRING,
  sourceipaddress STRING,
  useragent STRING,
  errorcode STRING,
  errormessage STRING,
  requestparameters STRING,
  responseelements STRING,
  additionaleventdata STRING,
  requestid STRING,
  eventid STRING,
  resources ARRAY<STRUCT<
    ARN:STRING,
    accountId:STRING,
    type:STRING>>,
  eventtype STRING,
  apiversion STRING,
  readonly STRING,
  recipientaccountid STRING,
  serviceeventdetails STRING,
  sharedeventid STRING,
  vpcendpointid STRING
)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://pocketbook-cloudtrail-logs/AWSLogs/123456789012/CloudTrail/';

Query with Athena:

sql
-- Find all API calls by specific user in last 7 days
SELECT eventtime, eventname, eventsource, sourceipaddress
FROM cloudtrail_logs
WHERE useridentity.username = 'admin@pocketbook.com'
  AND eventtime > date_format(date_add('day', -7, now()), '%Y-%m-%dT%H:%i:%sZ')
ORDER BY eventtime DESC;

-- Track resource deletions
SELECT eventtime, eventname, useridentity.arn, requestparameters
FROM cloudtrail_logs
WHERE eventname LIKE '%Delete%'
  AND eventtime > date_format(date_add('day', -30, now()), '%Y-%m-%dT%H:%i:%sZ')
ORDER BY eventtime DESC;

Security Monitoring

Common Security Queries

sql
-- Detect root account usage
SELECT eventtime, sourceipaddress, useragent
FROM cloudtrail_logs
WHERE useridentity.type = 'Root'
  AND eventtime > date_format(date_add('day', -1, now()), '%Y-%m-%dT%H:%i:%sZ');

-- Detect failed login attempts
SELECT eventtime, sourceipaddress, COUNT(*) as attempts
FROM cloudtrail_logs
WHERE eventname = 'ConsoleLogin'
  AND errormessage = 'Failed authentication'
  AND eventtime > date_format(date_add('hour', -1, now()), '%Y-%m-%dT%H:%i:%sZ')
GROUP BY eventtime, sourceipaddress
HAVING COUNT(*) > 5;

-- Track privilege escalation attempts
SELECT eventtime, useridentity.arn, eventname, requestparameters
FROM cloudtrail_logs
WHERE (eventname LIKE 'Put%Policy' OR eventname LIKE 'Attach%Policy')
  AND eventtime > date_format(date_add('day', -7, now()), '%Y-%m-%dT%H:%i:%sZ');

Compliance Reporting

Generate Compliance Reports

typescript
// scripts/generate-compliance-report.ts
import { CloudTrailClient, LookupEventsCommand } from '@aws-sdk/client-cloudtrail';
import { writeFileSync } from 'fs';

const client = new CloudTrailClient({ region: 'us-east-1' });

async function generateComplianceReport(startDate: Date, endDate: Date) {
  const events = await client.send(
    new LookupEventsCommand({
      StartTime: startDate,
      EndTime: endDate,
      MaxResults: 1000,
    })
  );

  const report = {
    period: {
      start: startDate.toISOString(),
      end: endDate.toISOString(),
    },
    summary: {
      totalEvents: events.Events?.length || 0,
      userActions: events.Events?.filter(
        (e) => e.Username !== 'AWSService'
      ).length,
      automatedActions: events.Events?.filter(
        (e) => e.Username === 'AWSService'
      ).length,
    },
    events: events.Events?.map((e) => ({
      time: e.EventTime,
      name: e.EventName,
      user: e.Username,
      resource: e.Resources?.[0]?.ResourceName,
    })),
  };

  writeFileSync(
    `compliance-report-${startDate.toISOString().split('T')[0]}.json`,
    JSON.stringify(report, null, 2)
  );

  console.log('Compliance report generated');
}

// Generate monthly report
const now = new Date();
const lastMonth = new Date(now);
lastMonth.setMonth(lastMonth.getMonth() - 1);

generateComplianceReport(lastMonth, now);

Best Practices

  1. Enable multi-region trails: Capture events from all regions
  2. Enable log file validation: Detect log tampering
  3. Use separate S3 bucket: Dedicated bucket for CloudTrail logs
  4. Enable MFA delete: Protect logs from deletion
  5. Set up lifecycle policies: Archive old logs to Glacier
  6. Monitor critical events: Alert on security-relevant events
  7. Regular log analysis: Review logs weekly for anomalies
  8. Integrate with SIEM: Forward logs to security tools
  9. Restrict access: Limit who can access CloudTrail logs
  10. Document procedures: Maintain runbooks for incident response

Cost Optimization

bash
# Enable S3 Intelligent-Tiering
aws s3api put-bucket-intelligent-tiering-configuration \
  --bucket pocketbook-cloudtrail-logs \
  --id cloudtrail-tiering \
  --intelligent-tiering-configuration '{
    "Id": "cloudtrail-tiering",
    "Status": "Enabled",
    "Tierings": [
      {
        "Days": 90,
        "AccessTier": "ARCHIVE_ACCESS"
      },
      {
        "Days": 180,
        "AccessTier": "DEEP_ARCHIVE_ACCESS"
      }
    ]
  }'

Troubleshooting

Logs Not Appearing

  1. Check CloudTrail is enabled and logging
  2. Verify S3 bucket policy allows CloudTrail writes
  3. Check IAM permissions
  4. Verify bucket exists and is in correct region

Query Athena for Missing Logs

sql
SELECT DISTINCT date_format(from_iso8601_timestamp(eventtime), '%Y-%m-%d') as log_date
FROM cloudtrail_logs
WHERE eventtime > date_format(date_add('day', -30, now()), '%Y-%m-%dT%H:%i:%sZ')
ORDER BY log_date DESC;

Next Steps

Resources

Released under the MIT License.