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:
- No child data without verified parental consent — full COPPA compliance
- Least privilege everywhere — every service, function, and role gets exactly the permissions it needs and nothing more
- Encryption at rest and in transit — no exceptions
- Defense in depth — WAF at the edge, security groups in the VPC, IAM at the API level
- Automated threat detection — GuardDuty monitors 24/7
- Secrets never in code — Secrets Manager with automatic rotation
This is Part 8 of the AWS Full-Stack Mastery series.
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');
}
}
COPPA Consent Flow
// 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:
- Why AWS & Getting Started (Part 1)
- Infrastructure as Code (CDK) (Part 2)
- Frontend (Amplify + CloudFront) (Part 3)
- Backend (API Gateway + Lambda + Fargate) (Part 4)
- Database (Aurora + DynamoDB + ElastiCache) (Part 5)
- AI/ML (Bedrock + SageMaker) (Part 6)
- DevOps (CodePipeline + CodeBuild) (Part 7)
- Security (IAM + Cognito + WAF) (this post)
- Observability (CloudWatch + X-Ray) (Part 9)
- Production (Multi-Region + DR) (Part 10)
References
- IAM Best Practices — Least privilege and role design.
- Amazon Cognito User Pools — Authentication and user management.
- COPPA Rule (FTC) — Official COPPA regulations.
- AWS WAF Developer Guide — Web application firewall configuration.
- AWS Managed Rules for WAF — Pre-built rule groups.
- KMS Developer Guide — Encryption key management.
- Secrets Manager Rotation — Automated secret rotation.
- GuardDuty User Guide — Intelligent threat detection.
- Cognito Advanced Security — Risk-based adaptive authentication.
- AWS Security Hub — Aggregated security findings and compliance.