Security on a children’s educational platform is not optional, and it’s not something you “add later.” COPPA (Children’s Online Privacy Protection Act) violations carry fines starting at $50,000 per incident. In 2023, the FTC fined Epic Games $275 million for COPPA violations related to Fortnite’s default privacy settings for children. Amazon (Ring) paid $5.8 million. We are not going to be on that list.

Kids Learn’s security posture is built on six principles:

  1. No child data without verified parental consent — full COPPA compliance
  2. Least privilege everywhere — every service, function, and role gets exactly the permissions it needs and nothing more
  3. Encryption at rest and in transit — no exceptions
  4. Defense in depth — WAF at the edge, security groups in the VPC, IAM at the API level
  5. Automated threat detection — GuardDuty monitors 24/7
  6. Secrets never in code — Secrets Manager with automatic rotation

This is Part 8 of the AWS Full-Stack Mastery series.

Security architecture — Defense in depth from edge WAF to VPC security groups to IAM policies

Cognito — Authentication with COPPA Compliance

User Pool Design

Kids Learn has two types of users: parents (who manage accounts, provide consent, track progress) and children (who use the learning platform). Under COPPA, we cannot collect personal information from children under 13 without verifiable parental consent. Our Cognito design reflects this:

// lib/stacks/security-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as guardduty from 'aws-cdk-lib/aws-guardduty';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import { EnvironmentConfig } from '../config/environments';

export class SecurityStack extends cdk.Stack {
  public readonly userPool: cognito.UserPool;
  public readonly webAcl: wafv2.CfnWebACL;
  public readonly encryptionKey: kms.Key;

  constructor(scope: Construct, id: string, props: { 
    config: EnvironmentConfig; 
    vpc: ec2.Vpc;
  } & cdk.StackProps) {
    super(scope, id, props);

    const { config } = props;

    // =========================================
    // KMS — Customer Managed Key
    // =========================================
    this.encryptionKey = new kms.Key(this, 'KidsLearnKey', {
      alias: `kidslearn-${config.envName}`,
      description: 'Encryption key for Kids Learn data at rest',
      enableKeyRotation: true, // Automatic annual rotation
      removalPolicy: config.enableDeletionProtection
        ? cdk.RemovalPolicy.RETAIN
        : cdk.RemovalPolicy.DESTROY,
    });

    // =========================================
    // Cognito User Pool — Parents Only
    // =========================================
    this.userPool = new cognito.UserPool(this, 'ParentUserPool', {
      userPoolName: `kidslearn-parents-${config.envName}`,
      
      // Sign-in configuration
      signInAliases: { email: true },
      selfSignUpEnabled: true,
      
      // Password policy
      passwordPolicy: {
        minLength: 12,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: true,
        tempPasswordValidity: cdk.Duration.days(3),
      },
      
      // MFA
      mfa: cognito.Mfa.OPTIONAL,
      mfaSecondFactor: {
        sms: true,
        otp: true,
      },
      
      // Account recovery
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
      
      // User verification
      userVerification: {
        emailSubject: 'Kids Learn — Verify your email',
        emailBody: 'Hello! Your verification code is {####}. Enter this code to complete your registration.',
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
      
      // Custom attributes for COPPA
      customAttributes: {
        'coppa_consent': new cognito.BooleanAttribute({ mutable: true }),
        'coppa_consent_date': new cognito.StringAttribute({ mutable: true }),
        'coppa_consent_method': new cognito.StringAttribute({ mutable: true }),
        'family_id': new cognito.StringAttribute({ mutable: false }),
      },
      
      // Advanced security
      advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
      
      // Deletion protection
      deletionProtection: config.enableDeletionProtection,
      
      removalPolicy: config.enableDeletionProtection
        ? cdk.RemovalPolicy.RETAIN
        : cdk.RemovalPolicy.DESTROY,
    });

    // App client
    const appClient = this.userPool.addClient('WebAppClient', {
      userPoolClientName: 'kidslearn-web',
      authFlows: {
        userSrp: true,
        userPassword: false, // Disable password auth — SRP only
      },
      oAuth: {
        flows: { authorizationCodeGrant: true },
        scopes: [cognito.OAuthScope.OPENID, cognito.OAuthScope.EMAIL, cognito.OAuthScope.PROFILE],
        callbackUrls: [
          `https://${config.envName === 'production' ? '' : config.envName + '.'}kidslearn.app/auth/callback`,
        ],
        logoutUrls: [
          `https://${config.envName === 'production' ? '' : config.envName + '.'}kidslearn.app`,
        ],
      },
      accessTokenValidity: cdk.Duration.minutes(60),
      idTokenValidity: cdk.Duration.minutes(60),
      refreshTokenValidity: cdk.Duration.days(30),
      preventUserExistenceErrors: true,
    });

    // =========================================
    // WAF — Web Application Firewall
    // =========================================
    if (config.enableWaf) {
      this.webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
        name: `kidslearn-waf-${config.envName}`,
        scope: 'REGIONAL', // Use CLOUDFRONT for CloudFront distributions
        defaultAction: { allow: {} },
        
        visibilityConfig: {
          cloudWatchMetricsEnabled: true,
          metricName: `kidslearn-waf-${config.envName}`,
          sampledRequestsEnabled: true,
        },
        
        rules: [
          // AWS Managed Rules — Common Rule Set
          {
            name: 'AWSManagedRulesCommonRuleSet',
            priority: 1,
            overrideAction: { none: {} },
            statement: {
              managedRuleGroupStatement: {
                vendorName: 'AWS',
                name: 'AWSManagedRulesCommonRuleSet',
              },
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName: 'CommonRuleSet',
              sampledRequestsEnabled: true,
            },
          },
          
          // SQL Injection protection
          {
            name: 'AWSManagedRulesSQLiRuleSet',
            priority: 2,
            overrideAction: { none: {} },
            statement: {
              managedRuleGroupStatement: {
                vendorName: 'AWS',
                name: 'AWSManagedRulesSQLiRuleSet',
              },
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName: 'SQLiRuleSet',
              sampledRequestsEnabled: true,
            },
          },
          
          // Known bad inputs
          {
            name: 'AWSManagedRulesKnownBadInputsRuleSet',
            priority: 3,
            overrideAction: { none: {} },
            statement: {
              managedRuleGroupStatement: {
                vendorName: 'AWS',
                name: 'AWSManagedRulesKnownBadInputsRuleSet',
              },
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName: 'BadInputs',
              sampledRequestsEnabled: true,
            },
          },
          
          // Rate limiting
          {
            name: 'RateLimit',
            priority: 4,
            action: { block: {} },
            statement: {
              rateBasedStatement: {
                limit: 2000, // 2000 requests per 5 minutes per IP
                aggregateKeyType: 'IP',
              },
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName: 'RateLimit',
              sampledRequestsEnabled: true,
            },
          },
          
          // Bot control
          {
            name: 'AWSManagedRulesBotControlRuleSet',
            priority: 5,
            overrideAction: { none: {} },
            statement: {
              managedRuleGroupStatement: {
                vendorName: 'AWS',
                name: 'AWSManagedRulesBotControlRuleSet',
                managedRuleGroupConfigs: [{
                  awsManagedRulesBotControlRuleSet: {
                    inspectionLevel: 'COMMON',
                  },
                }],
              },
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName: 'BotControl',
              sampledRequestsEnabled: true,
            },
          },
        ],
      });
    }

    // =========================================
    // GuardDuty — Threat Detection
    // =========================================
    if (config.envName === 'production') {
      new guardduty.CfnDetector(this, 'GuardDuty', {
        enable: true,
        dataSources: {
          s3Logs: { enable: true },
          kubernetes: { auditLogs: { enable: false } },
          malwareProtection: {
            scanEc2InstanceWithFindings: {
              ebsVolumes: true,
            },
          },
        },
        findingPublishingFrequency: 'FIFTEEN_MINUTES',
      });
    }

    // =========================================
    // Secrets Manager — Database Credentials Rotation
    // =========================================
    const dbSecret = new secretsmanager.Secret(this, 'ApiKeys', {
      secretName: `${config.envName}/kidslearn/api-keys`,
      description: 'API keys for third-party integrations',
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ service: 'kidslearn' }),
        generateStringKey: 'apiKey',
        excludePunctuation: true,
        passwordLength: 32,
      },
    });

    // =========================================
    // Tags
    // =========================================
    cdk.Tags.of(this).add('Project', 'KidsLearn');
    cdk.Tags.of(this).add('Environment', config.envName);
    cdk.Tags.of(this).add('Stack', 'Security');
  }
}
// src/lambda/auth/coppa-consent.ts
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { 
  CognitoIdentityProviderClient, 
  AdminUpdateUserAttributesCommand 
} from '@aws-sdk/client-cognito-identity-provider';

const cognito = new CognitoIdentityProviderClient({});

/**
 * COPPA-compliant parental consent verification.
 * Under COPPA, we must use a method that is "reasonably calculated" to
 * ensure the person providing consent is actually the child's parent.
 * 
 * Accepted methods:
 * 1. Credit card verification (small charge + refund)
 * 2. Government ID verification
 * 3. Signed consent form
 * 4. Video conference call
 * 5. Phone call to a trained representative
 */
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const body = JSON.parse(event.body || '{}');
  const { userId, consentMethod, verificationData } = body;

  // Verify the consent method
  let isVerified = false;
  
  switch (consentMethod) {
    case 'credit_card':
      // Verify a small charge was processed and matched
      isVerified = await verifyCreditCardConsent(verificationData);
      break;
    case 'signed_form':
      // Verify a digitally signed consent form
      isVerified = await verifySignedForm(verificationData);
      break;
    default:
      return {
        statusCode: 400,
        body: JSON.stringify({ error: 'Invalid consent method' }),
      };
  }

  if (!isVerified) {
    return {
      statusCode: 403,
      body: JSON.stringify({ error: 'Consent verification failed' }),
    };
  }

  // Update Cognito user attributes
  await cognito.send(new AdminUpdateUserAttributesCommand({
    UserPoolId: process.env.USER_POOL_ID!,
    Username: userId,
    UserAttributes: [
      { Name: 'custom:coppa_consent', Value: 'true' },
      { Name: 'custom:coppa_consent_date', Value: new Date().toISOString() },
      { Name: 'custom:coppa_consent_method', Value: consentMethod },
    ],
  }));

  return {
    statusCode: 200,
    body: JSON.stringify({ 
      message: 'Parental consent verified successfully',
      consentDate: new Date().toISOString(),
    }),
  };
};

IAM — Least Privilege Policies

Lambda Function IAM — Scoped to Exactly What’s Needed

// lib/constructs/scoped-lambda-role.ts
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export function createLessonsApiRole(
  scope: Construct,
  fn: lambda.Function,
  config: {
    dbSecretArn: string;
    sessionTableArn: string;
    mediaBucketArn: string;
  }
): void {
  // Read database credentials — specific secret only
  fn.addToRolePolicy(new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ['secretsmanager:GetSecretValue'],
    resources: [config.dbSecretArn],
  }));

  // DynamoDB — read/write to session table only
  fn.addToRolePolicy(new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
      'dynamodb:GetItem',
      'dynamodb:PutItem',
      'dynamodb:Query',
      'dynamodb:UpdateItem',
    ],
    resources: [
      config.sessionTableArn,
      `${config.sessionTableArn}/index/*`,
    ],
  }));

  // S3 — read-only access to media bucket
  fn.addToRolePolicy(new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ['s3:GetObject'],
    resources: [`${config.mediaBucketArn}/*`],
  }));

  // X-Ray tracing
  fn.addToRolePolicy(new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
      'xray:PutTraceSegments',
      'xray:PutTelemetryRecords',
    ],
    resources: ['*'],
  }));

  // Explicitly deny everything else. CDK's grant methods handle
  // the positive permissions; this denies any accidental over-grants.
}

The Bottom Line

Security for a children’s platform requires:

  • COPPA compliance built into the authentication flow, not bolted on afterward
  • Least-privilege IAM with explicit, scoped permissions for every function
  • Defense in depth — WAF at the edge, security groups at the network level, IAM at the API level
  • Encryption everywhere — KMS for data at rest, TLS 1.3 for data in transit
  • Automated monitoring — GuardDuty running 24/7, alerting on anomalies

In Part 9, we build observability — CloudWatch dashboards, X-Ray distributed tracing, structured logging, and cost management.

See you in Part 9.


This is Part 8 of a 10-part series: AWS Full-Stack Mastery for Technical Leads.

Series outline:

  1. Why AWS & Getting Started (Part 1)
  2. Infrastructure as Code (CDK) (Part 2)
  3. Frontend (Amplify + CloudFront) (Part 3)
  4. Backend (API Gateway + Lambda + Fargate) (Part 4)
  5. Database (Aurora + DynamoDB + ElastiCache) (Part 5)
  6. AI/ML (Bedrock + SageMaker) (Part 6)
  7. DevOps (CodePipeline + CodeBuild) (Part 7)
  8. Security (IAM + Cognito + WAF) (this post)
  9. Observability (CloudWatch + X-Ray) (Part 9)
  10. Production (Multi-Region + DR) (Part 10)

References

Export for reading

Comments