
Ferpa Reviewer
- 8 installs
- 9 repo stars
- Updated July 29, 2026
- aws-samples/sample-aws-resilience-skill
ferpa-reviewer is a skill that assesses AWS environments against FERPA and state student-privacy laws, performs gap analyses, and produces readiness reports.
About
ferpa-reviewer assesses AWS environments against FERPA and common state student-privacy laws, performing gap analyses and producing readiness reports for EdTech vendors and K-12 or higher-ed institutions handling student education records. A developer uses it to run a read-only four-phase assessment mapped to FERPA Control Domains and get a remediation roadmap. It is an assessment aid, not a compliance certification, and halts if credentials carry write permissions.
- Assesses AWS environments against FERPA and common state student-privacy laws
- Read-only 4-phase gap assessment mapped to FERPA Control Domains 1-10
- Targets EdTech vendors and K-12 or higher-ed institutions handling student records
Ferpa Reviewer by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,681 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
ferpa-reviewer capabilities & compatibility
- Capabilities
- compliance review · ferpa assessment · gap analysis · security audit
- Works with
- aws
- Use cases
- security audit
What ferpa-reviewer says it does
You are a FERPA readiness reviewer for AWS environments that handle student education records.
This skill is an assessment aid — not a compliance certification tool.
npx skills add https://github.com/aws-samples/sample-aws-resilience-skill --skill ferpa-reviewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 29, 2026 |
| Repository | aws-samples/sample-aws-resilience-skill ↗ |
What it does
Assess an AWS environment for FERPA and state student-privacy readiness and produce a remediation roadmap.
Who is it for?
EdTech vendors and K-12 or higher-ed institutions handling student education records on AWS
Skip if: A compliance certification or legal advice; findings require validation by counsel and compliance officers
When should I use this skill?
Assessing FERPA compliance, reviewing student-data privacy, or preparing for a state contract review or SPPO complaint
What you get
A FERPA gap assessment report and remediation roadmap across the relevant control domains
- a FERPA gap assessment report
- a remediation roadmap
- a readiness checklist
By the numbers
- FERPA Control Domains 1-10
- 4-phase assessment
- read-only Describe/Get/List calls
Files
FERPA Reviewer Skill
You are a FERPA readiness reviewer for AWS environments that handle student education records. You help the user — an EdTech vendor operating under a "school official" designation, a K-12 district, or a higher-ed institution — assess whether their AWS environment meets FERPA's "reasonable methods" safeguard expectations (34 CFR Part 99, PTAC guidance, and the NIST SP 800-171 baseline most state contracts adopt), identify potential gaps that could be cited in a state-contract annual review, SOC 2 Type II engagement, or U.S. Department of Education Student Privacy Policy Office (SPPO) complaint investigation, and produce a remediation roadmap to guide their compliance journey.
⚠️ Disclaimer: This skill is an assessment aid — not a compliance certification tool. Results are informational and do not constitute legal advice or guarantee compliance with FERPA or any state student-privacy law. Qualified legal counsel and institutional compliance officers must validate all findings before relying on them for audit or contractual purposes.
Guard rail — read-only only
All AWS operations in this skill are READ-ONLY (Describe / Get / List / BatchGet). Before any check runs, validate the caller's credentials against `references/credential-boundary.md`. If the credentials carry write permissions, HALT and tell the user why — a compliance tool that could mutate a production system containing student PII is a breach risk in itself, and most state EdTech contracts prohibit non-break-glass write access to production.
---
When to use which part of this skill
| User intent | Jump to |
|---|---|
| "Assess my env for FERPA" / gap assessment | Phase 1 — Bootstrap (start the automated flow) |
| "Quick FERPA breach-risk check" | Quick Scan mode — FCD 4, 5, 6, 7 only |
| "Can I sign this district DPA?" / pre-contract review | Standard mode |
| "Will I survive an SPPO complaint?" / annual attestation | Full mode — technical scan + questionnaire |
| Control-domain question ("What does FCD 4 require?") | `references/control-domains.md` |
| "What AWS services do I need for FERPA?" | `references/aws-service-mapping.md` |
| "Give me the FERPA readiness checklist" | `references/readiness-checklist.md` |
| State-specific question (CA/NY/TX/IL/etc.) | `references/state-law-addenda.md` |
| No AWS access — just Q&A | Answer from control-domains.md + aws-service-mapping.md without entering the phased flow |
---
4-Phase Assessment Flow
Phase 1: Bootstrap (~2 min) → Credential gate + scope confirmation (human-in-loop)
Phase 2: Discover (~10-25 min) → Per-FCD programmatic scan (automated)
Phase 3: Analyze (~5 min) → Gap consolidation + remediation roadmap (automated)
Phase 4: Report (~2 min) → Markdown (always) + HTML (on request)Full flow details in `references/workflow-overview.md`.
---
Phase 1 — Bootstrap (the only human-interaction phase)
1. Verify AWS CLI: aws --version. If missing, guide installation. 2. Get caller identity: aws sts get-caller-identity. Record account, region, principal ARN. 3. Credential boundary check (MANDATORY, non-skippable):
- Load `references/credential-boundary.md`
- Enumerate the principal's attached + inline policies
- Scan for blocked action verbs (
Create*,Update*,Delete*,Put*,Modify*,*) - If any found → emit the boundary violation message and HALT
4. Region residency check: Is the caller in a US region? Most state EdTech contracts require US-only data residency — non-US regions surface as a potential state-contract gap, even though FERPA itself has no region requirement. 5. Scope confirmation — ask the user:
- Role: Are you a (a) EdTech vendor operating as a school official, (b) K-12 district/school, or (c) higher-ed institution? — tailors the questionnaire and FCD emphasis
- Which account(s) and region(s)?
- Which US state(s) are the affected districts/students in? — affects state-law addenda (SOPIPA, Ed Law 2-d, SB 820, SOPPA, etc.)
- Which mode: Quick Scan / Standard / Full / Questionnaire-only?
- Student-record data stores to focus on (S3 buckets containing SIS exports, RDS instances behind the LMS, DynamoDB tables, OpenSearch domains) — needed for FCD4/7 scoping
- Is any data subject to COPPA (under-13 users)? — surface as a flag but scope-out of this skill (FERPA v1)
6. Emit the bootstrap summary before moving to Phase 2:
[BOOTSTRAP] Environment ready:
• AWS CLI: v2.x.x ✅
• Caller: arn:aws:iam::XXXX:role/ReadOnlyAssessment ✅
• Boundary: read-only (SecurityAudit) ✅
• Region residency: us-east-1 (US ✅ — no state residency flag)
• Account/Region: XXXX / us-east-1
• Role: EdTech vendor (school official under §99.31(a)(1)(i)(B))
• States in scope: CA, NY, TX (SOPIPA + Ed Law 2-d + SB 820 addenda apply)
• Mode: Standard (FCD 3, 4, 5, 6, 7, 8)
• Student-data stores declared: 3 S3 buckets, 1 Aurora cluster, 1 DynamoDB table
• COPPA scope: Yes — flagged for downstream review (out of skill scope)---
Phase 2 — Discover (automated)
Run programmatic checks per FCD in breach-risk-heat order, not numeric order. This ensures the findings with the highest complaint/breach-notification exposure surface first — a scan interrupted halfway through still produces a useful report.
Default order and per-mode coverage:
| Order | Control Domain | Check file | Quick | Standard | Full |
|---|---|---|---|---|---|
| 1 | FCD 4 — Auditing & Access Logging | `references/programmatic-checks/fcd-04-auditing.md` | ✅ | ✅ | ✅ |
| 2 | FCD 6 — Authentication | `references/programmatic-checks/fcd-06-authentication.md` | ✅ | ✅ | ✅ |
| 3 | FCD 7 — Encryption at Rest & In Transit | `references/programmatic-checks/fcd-07-encryption.md` | ✅ | ✅ | ✅ |
| 4 | FCD 5 — Access Control & Least Privilege | `references/programmatic-checks/fcd-05-access-control.md` | ✅ | ✅ | ✅ |
| 5 | FCD 3 — Disclosure Controls & Data Sharing | `references/programmatic-checks/fcd-03-disclosure-controls.md` | — | ✅ | ✅ |
| 6 | FCD 8 — Data Minimization, Retention & Destruction | `references/programmatic-checks/fcd-08-retention-destruction.md` | — | ✅ | ✅ |
| — | FCD 1, 2, 9, 10 | `references/readiness-checklist.md` | — | — | Questionnaire |
Execution rules
- Load check files on demand, one FCD at a time. Do NOT preload them — six files × ~200 lines each will bloat the context window.
- For each check, run the CLI command, capture the result, and classify severity per `references/severity-classification.md`:
BREACH RISK/COMPLIANCE GAP/HARDENING GAP/INFO. - Record result codes precisely:
COMPLIANT,NON_COMPLIANT,NOT_APPLICABLE,UNABLE_TO_ASSESS. These mean different things to an auditor or DPA reviewer — don't conflate them. - On
AccessDenied→ markUNABLE_TO_ASSESS, include the error, continue. Do NOT halt. - On
NoSuchEntity/ empty results for a resource type the user doesn't use →NOT_APPLICABLE, continue. - After each FCD, emit a one-paragraph summary before moving on:
[FCD 6 — Authentication] Complete:
• Checks executed: 8 (7 auto + 1 Identity Center manual)
• Findings: 1 BREACH RISK, 2 COMPLIANCE GAPS, 1 HARDENING GAP
• Top risk: 3 IAM users with console access to SIS data lack MFA (FCD6-03)---
Phase 3 — Analyze (automated)
1. Roll up per-FCD status per the rubric in `references/severity-classification.md`:
Non-Compliantif ≥1 Breach RiskAt Riskif ≥2 Compliance Gaps (or ≥1 Compliance Gap + ≥3 Hardening Gaps)Substantially Compliantif 0 Breach Risks and ≤1 Compliance GapCompliantif 0 Breach Risks and 0 Compliance Gaps
2. Build the priority matrix — for every finding, compute Priority = Severity weight × (1 / Fix effort). Sort descending. 3. Group remediation into the 4 roadmap buckets:
- Immediate (0-2 weeks) — Breach Risks + Quick Wins (high-severity + low-effort)
- Short-term (2-8 weeks) — Compliance Gaps
- Medium-term (2-6 months) — Hardening gaps requiring architectural change
- Long-term (6-12 months) — Organizational items (DPAs, training, incident-response tabletop)
4. Emit organizational questionnaire items for FCDs not covered by technical scan (FCD 1, 2, 9, 10) — pull from `references/readiness-checklist.md`. 5. State-law addendum rollup — for each state declared in Phase 1, surface the incremental requirements from `references/state-law-addenda.md` as an appendix in the report.
---
Phase 4 — Report
Generate the Markdown report using the fixed structure in `references/report-template.md`. Default output:
ferpa-reports/
└── ferpa-assessment-{YYYY-MM-DD}.mdThe template has 8 mandatory sections in a fixed order:
1. Assessment Metadata 2. Executive Summary (prose + summary table) 3. Per-FCD Findings (one subsection per assessed FCD) 4. Remediation Roadmap (4 phases) 5. Organizational Questionnaire (unassessed FCDs) 6. State-Law Addendum Rollup (per state in scope) 7. Appendix — Raw Check Results (full evidence) 8. Methodology & Caveats
Do not deviate from this structure. A consistent report shape is what makes these usable as DPA-review evidence and state-contract attestation support.
Optional HTML render
If the user wants a polished deliverable (for leadership, a district CIO, or an auditor):
python3 scripts/generate-html-report.py ferpa-reports/ferpa-assessment-{date}.mdProduces ferpa-assessment-{date}.html alongside the Markdown — no third-party deps required.
---
Assessment Modes
| Mode | FCDs | Time | When to suggest it |
|---|---|---|---|
| Quick Scan | FCD 4, 5, 6, 7 | ~10 min | "Am I at breach risk?" — hits the 4 FCDs driving the bulk of real-world EdTech data-breach notices |
| Standard (default) | FCD 3, 4, 5, 6, 7, 8 | ~25 min | Most gap assessments; covers all technically-assessable FCDs |
| Full | Standard + questionnaire for FCD 1, 2, 9, 10 | ~40 min | Pre-DPA signing, annual state-contract attestation, SPPO complaint response |
| Questionnaire-only | Organizational FCDs only | ~15 min | No AWS access, or vendor-side DPA review without infrastructure visibility |
Default to Standard. Offer Quick Scan proactively if the user says "quick" / "fast" / "just the critical stuff" or expresses breach anxiety.
---
FERPA Fundamentals (quick reference)
- Statute: 20 U.S.C. § 1232g (FERPA, 1974, amended most recently 2011)
- Regulation: 34 CFR Part 99
- Enforcement: U.S. Department of Education, Student Privacy Policy Office (SPPO) — complaint-driven, not scheduled. Ultimate penalty: withdrawal of federal education funding (never actually applied — SPPO pursues voluntary compliance)
- Applies to: Any educational agency or institution that receives federal education funds (all public K-12 districts, all public colleges, most private colleges). Also applies to EdTech vendors acting as "school officials" under the §99.31(a)(1)(i)(B) exception
- Education records covered: Records directly related to a student and maintained by the school or by a party acting for the school. Includes grades, transcripts, disciplinary records, health records held by the school, financial info, PII that could identify a student
- "Directory information" carve-out: Name, address, phone, date of birth, honors, etc., unless the parent/eligible student has opted out — still protected once opted out
- Audit/enforcement cadence: Complaint-driven (SPPO). State CSAs / departments of education often impose annual attestation on EdTech vendors via contract. SOC 2 Type II engagements annually.
- Key distinction from CJIS: FERPA is a federal statute + regulation, not a prescriptive security policy. "Reasonable methods" is the standard — what "reasonable" means in practice comes from PTAC guidance and the NIST 800-171 baseline most state contracts adopt.
- No federal breach notification: FERPA does not require breach notification. State laws do — all 50 states plus DC have breach-notification laws, all with different thresholds and timelines. See `references/state-law-addenda.md`.
- COPPA overlap: FERPA covers education records; COPPA covers under-13 online data collection. Almost every K-12 EdTech scenario touches both. This skill is FERPA-only — flag COPPA as a scope-out during Phase 1.
The 10 FERPA Control Domains (summary)
| FCD | Name | Technical? | Breach-risk heat |
|---|---|---|---|
| 1 | Directory Information & Consent Management | Partial | Medium |
| 2 | Student/Parent Rights (inspect, amend, opt-out) | No (organizational) | Low-Medium |
| 3 | Disclosure Controls & "School Official" Data Sharing | Yes | High |
| 4 | Auditing & Access Logging (§99.32 disclosure log) | Yes | Very High — #1 FERPA finding |
| 5 | Access Control & Least Privilege | Yes | High |
| 6 | Authentication (MFA, SSO, password hygiene) | Yes | High |
| 7 | Encryption at Rest & In Transit | Yes | High |
| 8 | Data Minimization, Retention & Secure Destruction | Partial | Medium |
| 9 | Incident Response & Breach Notification (state-law driven) | Partial | Medium |
| 10 | Vendor / Subprocessor Management (DPAs) | No | Medium |
Bolded FCDs are breach-risk heavy — always covered by every mode except Questionnaire-only.
---
Q&A mode (no AWS scan)
If the user asks a FERPA conceptual question and does not ask for an assessment, answer directly from the reference files without entering the phased flow:
- Control-domain specifics → `references/control-domains.md`
- "Which AWS service for X?" → `references/aws-service-mapping.md`
- Readiness / gap list → `references/readiness-checklist.md`
- State law question → `references/state-law-addenda.md`
Cite specific regulation sections when answering ("Per 34 CFR §99.31(a)(1)(i)(B), a school official exception is available to an outside party only if the party is performing a service the school would otherwise perform, is under direct control of the school, and meets the use and redisclosure restrictions of §99.33(a)...").
---
Reference files (load on demand)
| File | Purpose | When to load |
|---|---|---|
references/credential-boundary.md | Read-only IAM gate logic | Phase 1 |
references/workflow-overview.md | Full phase descriptions + error handling | When user asks "how does this work?" or you need the flow detail |
references/severity-classification.md | Severity levels (Breach / Compliance / Hardening / Info) + aggregate status rubric | Phase 2 (per check) and Phase 3 (rollup) |
references/report-template.md | Fixed report structure | Phase 4 |
references/programmatic-checks/fcd-03-disclosure-controls.md | Cross-account sharing, S3 external grants, resource policies | FCD 3 |
references/programmatic-checks/fcd-04-auditing.md | CloudTrail, Flow Logs, app-level §99.32 log | FCD 4 |
references/programmatic-checks/fcd-05-access-control.md | IAM policies, public exposure, Session Manager | FCD 5 |
references/programmatic-checks/fcd-06-authentication.md | MFA, password policy, key rotation, SSO | FCD 6 |
references/programmatic-checks/fcd-07-encryption.md | Encryption at rest + in transit: EBS, RDS, S3, KMS, DynamoDB, TLS | FCD 7 |
references/programmatic-checks/fcd-08-retention-destruction.md | S3 lifecycle, backup retention, crypto-shred | FCD 8 |
references/control-domains.md | Deep-dive guidance on all 10 FCDs | Q&A mode or when a specific FCD needs architectural context |
references/aws-service-mapping.md | FERPA requirement → AWS service matrix | Architecture questions |
references/readiness-checklist.md | Full readiness list by FCD | Questionnaire mode + Phase 3 organizational rollup |
references/state-law-addenda.md | SOPIPA (CA), Ed Law 2-d (NY), SB 820 (TX), SOPPA (IL) overlays | Phase 3 state rollup or when user asks a state-specific question |
Scripts
scripts/generate-html-report.py— render the Markdown report to self-contained HTML
FERPA Reviewer
FERPA compliance assessor for AWS environments handling student education records. Runs a read-only 4-phase assessment (Bootstrap → Discover → Analyze → Report) using Describe/Get/List AWS CLI calls, identifies gaps against the 10 FERPA Control Domains, and produces a remediation report.
Built for EdTech vendors operating as "school officials," K-12 districts, and higher-ed institutions. Covers 34 CFR Part 99, PTAC guidance, and the NIST SP 800-171 baseline most state contracts adopt.
Advisory tool, not a compliance determination. This skill is an assessment aid that helps identify potential gaps — it does not certify, attest to, or guarantee FERPA compliance. It is additive to (not a replacement for) qualified legal counsel, institutional compliance officers, and formal compliance regimes such as SOC 2 Type II engagements or state-contract annual reviews. Human judgement must validate all findings before any reliance for audit or contractual purposes.
When to use
Triggers on: "FERPA", "FERPA compliance", "student data privacy", "education records", "student PII", "FERPA assessment", "DPA review", "SPPO complaint", "PTAC checklist", "K-12 data privacy", "higher-ed data privacy", "SIS on AWS", "LMS compliance".
Also handles state-specific questions for SOPIPA (CA), Ed Law 2-d (NY), SB 820 (TX), and SOPPA (IL).
Assessment modes
| Mode | FCDs covered | Time | Use when |
|---|---|---|---|
| Quick Scan | 4, 5, 6, 7 | ~10 min | Breach-risk triage |
| Standard (default) | 3, 4, 5, 6, 7, 8 | ~25 min | Gap assessment, pre-contract review |
| Full | All 10 (scan + questionnaire) | ~40 min | DPA signing, annual attestation, SPPO response |
| Questionnaire-only | Organizational FCDs | ~15 min | No AWS access, paper-based review |
How to run
1. Prerequisites: AWS CLI installed, credentials with read-only access (e.g., SecurityAudit managed policy). The skill enforces a credential boundary check — it will refuse to run if write permissions are detected. 2. Start: Tell your AI coding assistant "Assess my environment for FERPA compliance" 3. Answer the bootstrap questions: Your role (vendor/K-12/higher-ed), account/region, US states in scope, scan mode, and which data stores hold student records 4. Wait: Phases 2-4 run automatically 5. Get the report: Output lands in ferpa-reports/ferpa-assessment-{YYYY-MM-DD}.md
Optional HTML render for a polished deliverable:
python3 scripts/generate-html-report.py ferpa-reports/ferpa-assessment-{date}.mdOutput
1. Per-FCD compliance status (Compliant / Substantially Compliant / At Risk / Non-Compliant) 2. Findings classified by severity (Breach Risk / Compliance Gap / Hardening Gap / Info) 3. Prioritized remediation roadmap in 4 phases (Immediate → Short-term → Medium-term → Long-term) 4. State-law addendum rollup for in-scope states 5. Organizational questionnaire for non-technical FCDs 6. Full evidence appendix (raw check results)
Guard rails
- Read-only only — all AWS operations are
Describe/Get/List/BatchGet. No mutations. - Credential boundary check — mandatory, non-skippable. If write permissions are detected, the skill halts before any checks run.
- COPPA scope-out — flags under-13 data for downstream review but does not assess COPPA compliance (FERPA-only in v1).
Reference files
Loaded on demand to keep context lean:
references/control-domains.md— deep-dive on all 10 FCDsreferences/aws-service-mapping.md— FERPA requirement → AWS service matrixreferences/readiness-checklist.md— full readiness list by FCDreferences/state-law-addenda.md— SOPIPA, Ed Law 2-d, SB 820, SOPPA overlaysreferences/severity-classification.md— severity levels and aggregate status rubricreferences/report-template.md— fixed 8-section report structurereferences/credential-boundary.md— read-only IAM gate logicreferences/programmatic-checks/fcd-{03-08}-*.md— per-FCD automated check definitions
See SKILL.md for the full skill definition and workflow details.
FERPA Requirements → AWS Service Mapping
Based on 34 CFR Part 99 (FERPA), PTAC "Data Security Checklist," and NIST SP 800-171.
Last verified against official sources: 2026-05-21.
Check https://studentprivacy.ed.gov/ for PTAC updates and https://www.ecfr.gov/current/title-34/subtitle-A/part-99 for regulation changes.
Complete mapping of FERPA and PTAC-derived safeguard requirements to specific AWS services and configurations.
FERPA itself is regulation, not a security framework — the mappings below draw from 34 CFR Part 99, PTAC "Data Security Checklist," NIST SP 800-171 (the baseline adopted by most state EdTech contracts), and common state-law-specific clauses.
---
Encryption (FCD 7)
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Encrypt student records at rest | KMS | Customer-managed keys (CMKs) for all student-data resources; automatic rotation enabled |
| EBS encryption | EBS + KMS | Enable default encryption at account level: aws ec2 enable-ebs-encryption-by-default |
| S3 encryption | S3 + KMS | Bucket policy denying s3:PutObject without s3:x-amz-server-side-encryption: aws:kms; enable Bucket Keys |
| RDS / Aurora encryption | RDS + KMS | Enable at creation — cannot retrofit; migrate via snapshot if needed |
| DynamoDB encryption | DynamoDB + KMS | Customer-managed CMK: SSESpecification: { SSEEnabled: true, SSEType: KMS, KMSMasterKeyId: <key> } |
| OpenSearch encryption | OpenSearch + KMS | At-rest encryption + node-to-node encryption, both with CMK |
| Backup encryption | AWS Backup + KMS | Backup plans reference CMK; cross-region copies retain encryption |
| TLS 1.2+ in transit | ACM, ALB/NLB, API Gateway, CloudFront | Enforce TLS 1.2 minimum via security policy (ELBSecurityPolicy-TLS13-1-2-2021-06) |
| S3 deny-plaintext | S3 bucket policy | Condition: {"Bool": {"aws:SecureTransport": "false"}} → Deny |
| RDS force-SSL | RDS parameter group | MySQL: require_secure_transport=1; Postgres: rds.force_ssl=1 |
| FIPS-validated endpoints (state contracts) | AWS FIPS endpoints | Set AWS_USE_FIPS_ENDPOINT=true or use <service>-fips.<region>.amazonaws.com |
| VPN encryption | Site-to-Site VPN | AES-256-GCM + SHA-256+ cipher suites |
Access Control & Authentication (FCD 5, 6)
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| MFA for admin access to student data | IAM Identity Center | Enable MFA requirement; prefer FIDO2 hardware + TOTP fallback |
| MFA enforcement on IAM principals | IAM | Policy condition: "Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "true"}} |
| Root MFA + no root access keys | IAM | Hardware MFA on root; delete root access keys; SCP denies root usage |
| Student/parent/teacher authentication | Cognito | Enable advanced security features (compromised-credentials, adaptive MFA) |
| District SSO federation | IAM Identity Center / Cognito SAML | SAML/OIDC to Google Workspace for Education, Okta, Azure AD, ClassLink, Clever |
| Least privilege | IAM + Access Analyzer | Access Analyzer: identify unused permissions; policies scoped to specific resources |
| RBAC | IAM Identity Center | Permission sets: StudentData-ReadOnly, StudentData-Operator, StudentData-Admin |
| Account lockout | Cognito / AD | Cognito advanced security: lockout policy. AD: Group Policy for 5-attempt lockout |
| Password policy | IAM | aws iam update-account-password-policy --minimum-password-length 14 --require-symbols --max-password-age 90 --password-reuse-prevention 10 |
| Session timeout (≤30 min) | IAM Identity Center | Set session duration on permission sets; STS role max session |
| No shared accounts | IAM Identity Center | One user per person; credential report for audit |
| Remote access encryption | Client VPN / SSM | Client VPN with MFA; SSM Session Manager for instance access (no SSH) |
| No public S3 / RDS / snapshots | S3 Block Public Access, RDS, EBS | Account-level S3 BPA; PubliclyAccessible=false on RDS; no public snapshot shares |
Disclosure Controls & Data Sharing (FCD 3)
| FERPA requirement | AWS service | Configuration |
|---|---|---|
| Isolation of student-data workloads | AWS Organizations + SCPs | Dedicated OU for student-data accounts; SCPs restrict services, regions |
| Explicit cross-account sharing | S3 bucket policies, RAM, Lake Formation | Named-principal allow-lists; log all shares |
| Detect external exposure | IAM Access Analyzer | Continuous scan across S3, RDS snapshots, KMS keys, Lambda, IAM roles, SQS/SNS, Secrets Manager |
| Network isolation | VPC, PrivateLink | Dedicated VPC per environment; no peering to non-student-data VPCs; VPC endpoints for AWS services |
| Prevent data egress to non-approved endpoints | VPC endpoint policies, egress firewall | Restrict s3:* to student-data buckets; Network Firewall with allow-list |
| ML/AI subprocessor control | Bedrock, SageMaker | Use models with no-retention data policies; log every training-job read from student-data S3 |
| §99.32 disclosure log (API-level) | CloudTrail data events | Enable data events on student-record S3 buckets + Lambda functions |
| §99.32 disclosure log (application-level) | CloudWatch Logs / S3 Object Lock | Dedicated, append-only log group/bucket; structured records |
Auditing & Logging (FCD 4)
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| API audit logging | CloudTrail | Enable in all regions; enable data events for student-data S3 buckets and Lambdas |
| Log integrity | CloudTrail | Enable log file validation |
| Log encryption | CloudTrail + KMS | Encrypt logs with CMK |
| Network flow logging | VPC Flow Logs | Enable for all student-data VPC subnets; send to CloudWatch Logs or S3 |
| Log retention | S3 / CloudWatch | Align with student-record retention schedule (typically 7+ years; some states require longer) |
| Log tamper-protection | S3 Object Lock | Governance or Compliance mode on log buckets |
| Centralized logging | CloudWatch, OpenSearch | Aggregate all logs in dedicated logging account |
| Log analysis / alerting | CloudWatch Alarms, EventBridge | Alert on: root login, MFA disable, SG changes, data-event anomalies |
| Log querying | Athena, CloudTrail Lake | Query CloudTrail and Flow Logs for investigations |
| §99.32 disclosure record | CloudWatch Logs + S3 Object Lock | Application-emitted structured events: {timestamp, student_id, requestor, purpose, exception, records[]} |
Threat Detection & Incident Response (FCD 9)
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Threat detection | GuardDuty | Enable in all student-data accounts and regions; enable S3 and EKS protection |
| Student-PII detection | Macie | Enable on student-data S3; create custom data identifiers matching your student-ID formats |
| Vulnerability scanning | Inspector | Enable for EC2, Lambda, ECR; auto-scan on deploy |
| Investigation | Detective | Enable for graph-based security investigation |
| Automated response | EventBridge + Lambda | Auto-isolate compromised instances, revoke leaked credentials, quarantine buckets |
| IR management | Incident Manager | Response plans and runbooks tailored to student-data incidents; include district-notification steps |
| Alerting | SNS, ChatBot, PagerDuty | Route Security Hub critical findings to IR team with on-call rotation |
| Breach-scope investigation | CloudTrail Lake + Athena | Query historical access patterns to determine impact scope |
Data Minimization & Retention (FCD 8)
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Minimize data collection | App-level + Macie | Macie to detect over-collected PII in S3 buckets |
| Automated retention | S3 Lifecycle, DynamoDB TTL, AWS Backup | Lifecycle rules per data category; TTL on DynamoDB; Backup lifecycle |
| WORM retention | S3 Object Lock | Governance (reversible) or Compliance (irreversible) mode |
| Secure destruction (NIST 800-88) | KMS key deletion (crypto-shred) | Schedule CMK deletion; 7-30 day wait; renders all encrypted copies unrecoverable |
| Physical media sanitization | AWS-managed | Documented in SOC 2 / FedRAMP — inherited control |
| Contract-termination destruction | Tag-based inventory + KMS deletion | Tag district:<name>; export if required; crypto-shred district-specific keys |
| Destruction attestation | Signed document | Out of AWS scope; log the key-deletion events from CloudTrail as evidence |
Configuration & Compliance Posture
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Configuration compliance | AWS Config | Enable recording; deploy managed rules for student-data checks |
| Conformance packs | AWS Config | Deploy packs: encryption checks, public access checks, logging checks |
| Patch management | Systems Manager Patch Manager | Patch baselines; maintenance windows; auto-approve critical patches |
| Desired state enforcement | SSM State Manager | Enforce CIS Benchmarks, agent installations |
| Image hardening | EC2 Image Builder + Inspector | Hardened AMIs from CIS/STIG; scan before publish |
| Container scanning | ECR + Inspector | Scan-on-push for container images |
| Change tracking | AWS Config + CloudTrail | Config records resource changes; CloudTrail records who made them |
| Approved services gate | Organizations SCPs | Deny non-approved AWS services in student-data accounts |
Compliance & Audit Evidence
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Compliance posture | Security Hub | Enable; review findings dashboard regularly |
| Automated evidence | Audit Manager | Create assessment mapping to NIST 800-171 / CIS Controls; schedule evidence collection |
| AWS compliance reports | AWS Artifact | Download SOC 2 Type II, ISO 27001, FedRAMP High, StateRAMP, TX-RAMP as DPA attachments |
| Resource inventory | AWS Config + SSM | Config: full resource inventory. SSM: managed instance inventory |
| Credential report | IAM | aws iam generate-credential-report — MFA status, key age, last login |
| Subprocessor registry | Tag + Config | Tag every external-facing resource with subprocessor:<name>; export register on demand |
Data Sharing & Egress Controls
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Data loss prevention | S3 BPA, VPC endpoints, Macie | Account-level S3 BPA; VPC endpoints prevent internet-path exfil; Macie flags anomalous PII movement |
| Private AWS API access | VPC Endpoints (PrivateLink) | Interface/gateway endpoints for S3, KMS, DynamoDB, CloudTrail, SSM |
| Egress filtering | Network Firewall | Domain allow-list for outbound HTTPS; deny by default |
| Cross-region replication scope | S3 CRR + KMS | Replicate only to approved regions; use in-region CMKs |
Mobile / Student-Facing Access
| FERPA / PTAC requirement | AWS service | Configuration |
|---|---|---|
| Student-facing app authentication | Cognito | MFA (where age-appropriate); SAML federation from district IdP |
| Virtual desktop for sensitive access | WorkSpaces | Deploy in student-data VPC; MFA; restrict clipboard/printing |
| Application streaming | AppStream 2.0 | Stream student-data apps; no local data storage |
| Secure admin remote access | Client VPN + SSM Session Manager | MFA, cert-based auth on VPN; SSM for shell access with full session logging |
State-Framework-Specific Hardening
| Requirement | AWS service | Configuration |
|---|---|---|
| US-only data residency (many state DPAs) | SCPs + Config | SCP deny non-US regions; Config rule flagging non-US resources |
| FIPS 140-2 validated crypto (StateRAMP, TX-RAMP L2) | FIPS endpoints + KMS | AWS_USE_FIPS_ENDPOINT=true; KMS HSMs are FIPS 140-2 Level 2 |
| Annual SOC 2 attestation | AWS Artifact + own SOC 2 | AWS Artifact for AWS SOC 2; engage auditor for own org's SOC 2 Type II |
| State-DOE approved vendor list | Organizational | Track status in contract system; not an AWS control |
See `state-law-addenda.md` for per-state incremental requirements.
FERPA Control Domains — Detailed AWS Implementation Guide
Based on 34 CFR Part 99 (FERPA), PTAC "Data Security Checklist," and NIST SP 800-171.
Last verified against official sources: 2026-05-21.
Check https://studentprivacy.ed.gov/ for PTAC updates and https://www.ecfr.gov/current/title-34/subtitle-A/part-99 for regulation changes.
Deep-dive on each of the 10 FERPA Control Domains (FCDs) with specific AWS implementation guidance.
FERPA itself does not enumerate "control domains" — the statute and 34 CFR Part 99 describe rights, exceptions, and a general "reasonable methods" safeguard expectation. The 10 FCDs used here distill FERPA, the ED Privacy Technical Assistance Center (PTAC) "Data Security Checklist," and the NIST SP 800-171 controls most state EdTech contracts adopt into assessment-sized buckets.
Where relevant, each FCD includes a K-12 note (district/school-as-customer perspective) and a Higher-ed note (institution-as-customer perspective) and a Vendor note (EdTech-as-school-official perspective).
---
FCD 1 — Directory Information & Consent Management
Requirements
- Identify which fields in your data model are "directory information" (per 34 CFR §99.3) — name, address, phone, DOB, awards, enrollment status, etc.
- Notify parents/eligible students annually of directory-information designations and opt-out rights (§99.37)
- Honor opt-outs — once a parent/student opts out, directory information becomes protected and cannot be disclosed without consent
- Track consent: for disclosures that require it (§99.30), maintain a record of the signed consent including what info, to whom, and for what purpose
AWS implementation
- Data model tagging: Tag S3 objects, DynamoDB items, and RDS columns that contain directory information vs. non-directory PII. Enables targeted policy enforcement.
- Amazon Macie custom data identifiers: Create identifiers matching your directory-info schema (student ID formats, names alongside school IDs) so you can detect leaks into non-approved buckets.
- Fine-grained access control: Lake Formation or row-level security in Redshift to expose only non-opted-out directory info for bulk uses.
- Consent store: Cognito user attributes, DynamoDB consent table, or external consent-management SaaS. The consent store itself is a student record and must itself meet all FERPA safeguards.
- EventBridge automation: When a consent record flips to "opted out," trigger a Lambda to update the student's data-access tags and invalidate any cached exports.
Key pitfall
Vendors frequently assume "directory information = public" — it is only not-protected if the parent/student has not opted out AND the school has designated the field as directory information in its annual notice. Treat it as protected until you can prove both conditions.
K-12 note
District is responsible for the annual notice and for collecting opt-outs. Vendor must honor opt-out signals from the district's SIS — test this end-to-end.
Higher-ed note
Student (now adult, ≥18 or in postsecondary attendance) holds the opt-out right directly. Typically managed via student self-service portal.
Vendor note
DPAs usually specify whether the vendor handles opt-out tracking or receives a filtered feed from the district. Know which — and log receipt of opt-out updates.
---
FCD 2 — Student/Parent Rights (Inspect, Amend, Opt-out)
Requirements
FERPA grants specific rights (§99.10–99.22):
- Inspect and review: The school must provide access to education records within 45 days of request
- Amend: Request amendment of records believed inaccurate; formal hearing process if denied (§99.20–99.22)
- Opt-out of directory disclosure: Covered in FCD 1
- Complaint: Right to file a complaint with SPPO
AWS implementation
This FCD is primarily organizational/procedural — AWS does not directly satisfy it. Supporting technical capabilities:
- Data subject access request (DSAR) tooling: Build an internal runbook that enumerates every data store containing a given student's records. Leverage tags + Athena queries over cataloged data lakes.
- Athena / AWS Glue Data Catalog: Federate queries across S3, RDS, DynamoDB to produce a single student's full record on request.
- Audit log of DSARs: Log every student-initiated access/amendment request — itself becomes part of the §99.32 record.
- Amendment workflow: Typically a ticketing system (Jira, ServiceNow) with an integration that writes amended records back to the SIS — CloudTrail captures the API calls; the ticketing system captures the human-authorization chain.
Key pitfall
"I can't find all of student X's records" is a real operational failure that becomes a FERPA finding if a DSAR can't be completed in 45 days. Data-lineage tagging discipline is the AWS-side preventive.
Vendor note
DPA typically makes the district the point of contact for DSARs, with the vendor required to fulfill within a contracted SLA (often 10 business days to give the district time to turn it around to the parent).
---
FCD 3 — Disclosure Controls & "School Official" Data Sharing
Requirements
Under §99.31(a), education records may be disclosed without consent in specific exceptions, the most operationally important being:
- §99.31(a)(1)(i)(B) "School Official" exception: An outside party (vendor) may be treated as a school official if:
1. It performs an institutional service/function for which the school would otherwise use its own employees 2. It is under the direct control of the school regarding the use and maintenance of education records 3. It is subject to the §99.33(a) use-and-redisclosure restrictions
- §99.33(a) Redisclosure: Cannot redisclose without consent except under another §99.31 exception
- §99.32 Record of disclosures: The school (and any party acting for the school) must maintain a record of each request for access and each disclosure of education records
AWS implementation
- Separation of student-record environments: Use dedicated AWS accounts (ideally a separate OU under AWS Organizations) for student-record workloads. Apply SCPs to enforce region, service, and data-residency constraints.
- Cross-account sharing controls:
- S3 bucket policies with explicit allow-list of school AWS account principals; deny all other cross-account access
- RAM (Resource Access Manager) shares tracked and reviewed; no shares to unaffiliated accounts
- Lake Formation cross-account data-catalog shares logged via CloudTrail
- AWS Organizations + SCPs: Deny any action that would share a student-record resource with a non-approved principal, e.g.,
s3:PutBucketPolicywith conditions blocking*-principal grants. - IAM Access Analyzer: Continuously scan for cross-account / external-principal exposure of S3, RDS snapshots, KMS keys, Lambda functions, IAM roles. Any unexpected finding = a potential §99.33 redisclosure event.
- Subprocessor management (ties to FCD 10): If a third-party SaaS or another AWS account is a subprocessor, document and restrict the connection. PrivateLink + VPC endpoints are the least-leaky pattern.
- §99.32 disclosure log: Application-level. Every export, download, API response that contains education records must log: date/time, requestor, purpose, records disclosed, authority (which §99.31 exception applied). CloudTrail logs the API call, not the semantic purpose — the application must emit the log. This is the single most common technical gap in vendor environments.
Key pitfall
The "direct control" prong of the school-official exception is where DPAs get tight — the district must retain control over how the vendor uses records. If the vendor unilaterally changes data retention, adds subprocessors, or uses records for R&D / model-training without district sign-off, the school-official designation is invalid and every resulting disclosure becomes an unauthorized disclosure.
Vendor note
Model-training on student data is the hottest current compliance topic. If you train ML models on student data, you must have explicit DPA language permitting it. AWS-side: log every training-job invocation that reads from a student-data S3 bucket; tie it back to a documented district authorization.
---
FCD 4 — Auditing & Access Logging
Requirements
- §99.32 Record of disclosures: Maintain a record of each request for access and each disclosure of education records from a student's record. Must include:
- Parties who requested/received the record
- Legitimate interest in the disclosure (which §99.31 exception or consent)
- Retention: §99.32(a)(2) — the record must be maintained with the education record as long as the record is maintained (effectively, for the life of the student record, often measured in decades)
- Security logging (PTAC/NIST 800-171 3.3.x): Log security-relevant events — authentication, privilege changes, data access, system events — with sufficient detail to reconstruct an incident
- Log integrity: Protect logs from modification
- Log review: Regular review of audit logs for anomalies
AWS implementation
- CloudTrail: Enable in all regions. Enable data events for S3 buckets containing student records and for Lambda functions that process them. Enable log file validation. Encrypt with KMS CMK.
- CloudTrail log storage: Dedicated logging account per AWS Organizations best practice. Enable S3 Object Lock (Governance or Compliance mode) to prevent log deletion.
- VPC Flow Logs: Enable for all student-data VPCs.
- CloudWatch Logs: Centralize application logs, including the §99.32 disclosure log. Retention ≥7 years is a common state-contract floor; longer if matching student-record retention.
- Athena / OpenSearch: Query CloudTrail and application logs for investigations and periodic review.
- §99.32 disclosure log — application-level (this is the critical gap):
- Every code path that returns education records to an external party emits a structured log event:
{timestamp, student_id, requestor, purpose, §99.31_exception, records_disclosed[]} - Store in a dedicated, append-only CloudWatch Logs group or S3 bucket with Object Lock
- Expose via a DSAR tool so a parent can see the record of disclosures about their child
- GuardDuty — detect anomalous access patterns (bulk data exfiltration, impossible-travel login, credential compromise).
- Security Hub — aggregate findings for periodic review.
Critical detail
AWS CloudTrail logs API calls. It does NOT log application-level student-record access. If a teacher queries a database for a student's grades, CloudTrail logs the API call to RDS but not which student's record was viewed, who viewed it, or why. Your application must implement its own §99.32 disclosure log — this is not optional under FERPA, it is a statutory requirement.
K-12 note
Districts often forget that the §99.32 record must survive re-procurements. If you switch LMS vendors, the disclosure log from the old vendor must be migrated, not deleted. DPAs should include a log-export clause.
Higher-ed note
University registrars' offices typically maintain the canonical disclosure log; connected systems (LMS, library, food-service) feed into it.
Vendor note
The disclosure log is audit evidence #1 in an SPPO complaint. Build it before you need it. Make it queryable by student ID so you can respond to "show me every time my child's record was disclosed" in minutes, not days.
---
FCD 5 — Access Control & Least Privilege
Requirements
- Need-to-know / least-privilege access to student records
- Role-based access control tied to job function (teacher sees their classes; counselor sees assigned caseload; registrar sees all; IT admin sees infrastructure but not content where possible)
- Account provisioning/de-provisioning tied to HR/SIS status
- Session timeout / inactivity lock
- Remote access only via encrypted channel
- No shared accounts — individual accountability
AWS implementation
- IAM Identity Center: Centralized access management with permission sets scoped to student-data accounts. Federate from the district/institution's IdP (Azure AD, Okta, Google Workspace for Education).
- Permission sets:
StudentData-ReadOnly— analysts, QAStudentData-Operator— scoped writes to specific servicesStudentData-Admin— break-glass, MFA + ticket-required conditions- SCPs: Restrict student-data accounts to approved AWS services and US regions. Deny
iam:PassRolepatterns that would enable privilege escalation. - S3 bucket policies: Restrict each student-data bucket to a named list of IAM role ARNs. Deny anonymous or cross-account access unless explicitly allowed.
- S3 Block Public Access: Enable at the account level for all student-data accounts. Non-negotiable.
- IAM Access Analyzer: Continuous external-access monitoring.
- Least privilege in application code: The application's own authz model (teacher → their classes → their students) is the first line. AWS IAM protects the infrastructure around it but can't enforce "teacher X can only see their own students" unless data is partitioned that way.
- Session timeout: IAM Identity Center session duration ≤8 hours; console role session ≤1 hour for admin roles; application session timeout ≤30 minutes of inactivity.
- Remote access: Client VPN with MFA + cert-based auth. SSM Session Manager for instance shell access (logged, auditable, no open SSH).
- No direct database access from engineers: Access via least-privilege read-only replicas or bastion + SSM with full session logging.
Key pitfall
"The app enforces row-level access" is only true if it actually does. PTAC guidance is clear that access controls must be tested. An attacker with any authenticated user's credentials should not be able to see records outside their authorized scope. Threat-model and pen-test accordingly.
---
FCD 6 — Authentication (MFA, SSO, Password Hygiene)
Requirements
FERPA does not specify authentication requirements — "reasonable methods." PTAC and NIST 800-171 establish the de facto baseline:
- Multi-factor authentication for all administrative access and for any access that touches bulk student records
- Strong password policy (length, complexity, rotation)
- Account lockout after failed attempts
- Unique IDs — no shared accounts
- Root/break-glass account hardening
AWS implementation
- IAM Identity Center: MFA required for all users. Support FIDO2 hardware tokens (preferred) and TOTP as fallback.
- IAM MFA enforcement: For any remaining IAM users, policy condition
"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "true"}}. - Root account: Hardware MFA enabled, access keys deleted, usage restricted via SCP, break-glass procedure documented.
- IAM password policy: Minimum 14 characters (state contracts commonly require 12–16), complexity enabled, 90-day max age, reuse prevention ≥10, lockout after 5 failed attempts.
- Cognito for end-user (student/parent/teacher) authentication: Enable advanced security features (compromised-credentials check, risk-based adaptive MFA). Integrate with district IdP via SAML federation where possible to avoid duplicate identity stores.
- SAML federation: For districts, federate vendor app logins from Google Workspace for Education or ClassLink / Clever rostering — student credentials never hit the vendor directly.
- Access key rotation: ≤90 days. Prefer IAM roles over long-lived keys.
Key pitfall — student-facing MFA
MFA for admins is straightforward. MFA for students (especially K-12 students without personal email or phone) is harder. Common compromise: passkeys (WebAuthn) on school-issued devices, or SSO from the district's IdP which handles MFA upstream. Don't just skip MFA for student-facing auth — design around the constraint.
Vendor note
When the district federates via SAML/OIDC, the vendor "authenticates" via the district's IdP. You still need MFA on your own admin backend. Don't confuse the two auth paths.
---
FCD 7 — Encryption at Rest & In Transit
Requirements
FERPA does not mandate encryption. PTAC guidance and every modern state EdTech DPA do.
- At rest: All student records encrypted. FIPS 140-2/3 validated modules preferred (required for StateRAMP Moderate, TX-RAMP Level 2, some other state frameworks).
- In transit: TLS 1.2+ for all traffic carrying student records, inside and outside the VPC.
- Key management: Customer-managed keys for auditability; automatic rotation enabled.
AWS implementation
- KMS: AWS KMS HSMs are FIPS 140-2 Level 2 validated. Use CMKs (not AWS-managed keys) for student-data resources to enable cross-account access logging, key-policy-based access control, and rotation visibility.
- EBS encryption: Enable default EBS encryption at the account level.
- RDS encryption: Enable at instance creation (cannot retrofit — must migrate via snapshot). Use CMK.
- S3 encryption: SSE-KMS with CMK. Bucket policy to deny
s3:PutObjectwithout encryption headers. Enable Bucket Keys to reduce KMS cost. - DynamoDB encryption: Enabled by default with AWS-owned keys. Switch to CMK for student-data tables.
- OpenSearch encryption: Enable at-rest encryption with CMK and node-to-node encryption.
- Backup encryption: AWS Backup with CMK; ensure snapshots and copies retain encryption.
- FIPS endpoints: For state frameworks requiring FIPS, set
AWS_USE_FIPS_ENDPOINT=trueor use<service>-fips.<region>.amazonaws.comendpoints. Most US regions support FIPS endpoints for KMS, S3, EC2, etc. - TLS enforcement:
- ALB/NLB: enforce TLS 1.2+ via security policy (
ELBSecurityPolicy-TLS13-1-2-2021-06or newer). - API Gateway: same.
- CloudFront: viewer protocol policy = redirect-to-https + min TLS 1.2.
- S3: bucket policy with
"aws:SecureTransport": "false"deny condition. - RDS/Aurora: parameter group
require_secure_transport=1(MySQL) orrds.force_ssl=1(Postgres). - Certificate management: ACM for public certs, ACM Private CA for internal. Automate renewal.
- Macie: Detect unencrypted or publicly-accessible student PII in S3.
Key pitfall — retrofit encryption
RDS, Aurora, OpenSearch, and ElastiCache cannot be encrypted after creation. If you discover an unencrypted production student-data DB, you must migrate via snapshot-restore. Plan the cutover carefully — this is a multi-hour operation.
---
FCD 8 — Data Minimization, Retention & Secure Destruction
Requirements
- Collect only the student data needed for the institutional purpose (PTAC minimization principle; reinforced by SOPIPA, SOPPA, Ed Law 2-d, and most DPAs)
- Define retention periods aligned with educational record retention schedules (state records retention schedules, often 5–7 years post-graduation for education records, shorter for incidental data)
- Secure destruction when retention expires — NIST SP 800-88 Rev 1 standards
- Deletion on DPA termination — most state DPAs require vendor to destroy or return data within 30–60 days of contract end
AWS implementation
- S3 Lifecycle Policies: Automate deletion or transition to Glacier Deep Archive. Configure per prefix matching the retention category.
- S3 Object Lock: Governance mode for retention-required records; Compliance mode where even root cannot delete before expiry.
- RDS automated backups: Configure retention period. Ensure deleted instances' final snapshots are also purged per schedule.
- AWS Backup: Define lifecycle rules on backup plans; test restore.
- DynamoDB TTL: Automated deletion of items past retention.
- Crypto-shred: For end-of-contract destruction, delete the KMS CMK that encrypts the student data (schedule key deletion, 7–30 day wait). This renders all encrypted records unrecoverable in all copies, including backups and DR sites. Document this as part of DPA offboarding.
- AWS media sanitization: AWS handles physical media destruction per NIST SP 800-88 — documented in SOC 2 and FedRAMP reports. Reference via AWS Artifact.
- Tag-based retention: Tag every student-data resource with a
retention-expirytag; Config rule to flag resources whose tag date has passed but resource still exists.
Key pitfall — backups and replicas
Deleting the primary copy does not delete backups, read replicas, cross-region replicas, audit logs containing exported data, or cached exports in analytics systems. The retention schedule must enumerate all locations. Crypto-shred solves most of this by making all encrypted copies inert simultaneously.
Vendor note — offboarding
When a district terminates a DPA, the vendor must destroy or return all district data within the contracted window. Practical pattern: 1. Export the district's data to a district-owned S3 bucket (return) 2. Delete district-specific tables/prefixes 3. Schedule deletion of the district's dedicated KMS key (crypto-shred any residual) 4. Produce a destruction attestation signed by the vendor's CISO
---
FCD 9 — Incident Response & Breach Notification
Requirements
- Documented incident response plan covering detection, containment, eradication, recovery
- FERPA itself has no breach notification requirement. State data-breach notification laws fill this gap — all 50 states + DC have them, all different.
- Most state EdTech contracts impose a contractual breach-notification timeline — commonly 24–72 hours for vendor-to-district, 30–60 days for district-to-parent
- Test the IR plan at least annually (tabletop or functional)
AWS implementation
- GuardDuty: Enable in all student-data accounts for threat detection (unusual data access, credential compromise, exfil indicators).
- Security Hub: Aggregate findings from GuardDuty, Inspector, Macie, Config.
- Detective: Graph-based investigation for breach confirmation.
- EventBridge + Lambda: Automate containment — isolate compromised EC2, revoke suspicious IAM sessions, rotate compromised credentials.
- SNS / PagerDuty / Opsgenie: Alert IR team on critical findings.
- Systems Manager Incident Manager: Formalize runbooks for student-data incidents; include district-notification steps with contact info.
- CloudTrail Lake: Query historical events during investigation; retain 7+ years.
- Breach-scope determination: The IR runbook must include a "scope determination" step — enumerate affected students, export of disclosure logs for impacted records, preserve evidence. This is the input to any state-law breach-notification decision.
State-law breach notification
See `state-law-addenda.md` for per-state specifics. The common dimensions to track in the IR plan:
- Trigger (what counts as "breach" — acquisition vs access vs reasonable belief)
- Timeline (CA: "most expedient time possible"; NY SHIELD: without unreasonable delay; TX: 60 days; etc.)
- Content (what the notice must say)
- Recipients (parents, AG, state ED dept, credit bureaus if >500)
- Documentation (what records to retain)
Vendor note — district-notification clause
DPAs typically specify a very fast vendor→district notification window (often ≤24 hours of discovery). Miss this, and the district misses its own window to parents. Build the auto-notify path into the IR runbook.
---
FCD 10 — Vendor / Subprocessor Management (Data Processing Agreements)
Requirements
- Every party receiving education records must be bound by FERPA-equivalent terms
- If the vendor uses subprocessors (other AWS accounts, third-party SaaS, ML model providers), those subprocessors must be disclosed to the school and bound by equivalent or more restrictive terms
- Annual review of vendor and subprocessor security posture
- Right to audit / inspect
AWS implementation
This FCD is primarily organizational/contractual — AWS does not directly satisfy it. Technical support:
- AWS Artifact: Pull AWS SOC 2 Type II, ISO 27001, FedRAMP High, StateRAMP, and TX-RAMP reports as inherited-control evidence to include in DPAs.
- AWS as subprocessor: AWS is a subprocessor to the vendor; the vendor is the school official. The vendor's DPA should list AWS in its subprocessor table with region and service scope.
- Subprocessor enumeration: Tag every AWS service in use + every third-party vendor with a "student-data-touching" flag. The list becomes the subprocessor table in the DPA.
- PrivateLink / VPC endpoints: Minimize the surface where third-party SaaS receives student data — keep traffic private where possible.
- Access review: Quarterly audit of: IAM roles, cross-account shares, RAM resources, Lake Formation grants. Every external-principal finding ties back to a named subprocessor.
Key pitfall — ML/AI subprocessors
Embedding a third-party LLM API (including AWS Bedrock with certain non-default models) into a student-facing workflow makes that LLM provider a subprocessor. If the provider retains prompts for model training, that's an unauthorized redisclosure under §99.33(a). Use models with explicit no-retention terms and document the data flow.
Vendor note
Maintain a living subprocessor register. When districts ask for it (they will), produce it within 5 business days. Missing subprocessors is an instant-credibility-killer in district security reviews.
---
Summary — Which FCDs drive real audit/complaint/breach findings?
Based on publicly-disclosed EdTech data breaches and SPPO complaint resolutions over the past 5 years:
| Rank | FCD | Why it tops the list |
|---|---|---|
| 1 | FCD 4 — Auditing | Missing or incomplete §99.32 disclosure logs; inability to answer "who accessed this student's record" |
| 2 | FCD 7 — Encryption | Unencrypted S3 buckets exposed to the internet — breach vector #1 in state AG notifications |
| 3 | FCD 6 — Authentication | Credential compromise on unMFAed admin accounts |
| 4 | FCD 5 — Access Control | Over-privileged IAM roles; public RDS snapshots |
| 5 | FCD 3 — Disclosure Controls | Unauthorized subprocessor disclosure; ML training without district consent |
| 6 | FCD 10 — Vendor management | Undisclosed subprocessors surfaced during breach notification |
FCDs 1, 2, 8, 9 are important but show up less often in post-breach or post-complaint analyses — usually because problems there are caught contractually before becoming incidents.
Credential Permission Boundary
Last verified: 2026-05-21.
Principle
The FERPA assessment operates in strict read-only mode. Credentials used to run programmatic checks MUST NOT have any write, modify, or delete permissions — a compliance tool that could accidentally mutate a production system containing student education records is itself a breach risk.
This is a non-skippable gate at Phase 1 (Bootstrap). If the check fails, HALT the assessment and ask the user for compliant credentials.
Why this matters for FERPA specifically
- Student records are protected the moment the environment touches them. Unlike infrastructure that can be "restored from backup," an unauthorized modification to a student record is itself a potential §99.32 misdisclosure event — the tool that caused it must then be logged as the party that made the change.
- State EdTech DPAs commonly prohibit non-break-glass write access to production. A vendor running this skill with write-capable creds may be violating its own DPA with every district in the contract.
- Incident response posture. A production environment containing education records is often under elevated change control after a security event. A read-only audit tool fits cleanly into that posture; a write-capable one does not.
- State AG breach-notification statutes are triggered by unauthorized acquisition or access. Even a well-intentioned accidental write by an assessment tool could surface in breach-scope analysis. Don't create that problem.
Allowed IAM policies
| Policy ARN | Description |
|---|---|
arn:aws:iam::aws:policy/ReadOnlyAccess | Full read-only across all services |
arn:aws:iam::aws:policy/ViewOnlyAccess | View-only (slightly more restrictive) |
arn:aws:iam::aws:policy/SecurityAudit | Security-focused read-only — recommended for this skill |
| Custom read-only policy | Must contain ONLY Describe / Get / List / BatchGet actions |
Blocked IAM policies
| Policy ARN / Pattern | Reason |
|---|---|
arn:aws:iam::aws:policy/AdministratorAccess | Full admin — never acceptable |
arn:aws:iam::aws:policy/PowerUserAccess | Write access to most services |
Any *:Create*, *:Update*, *:Delete*, *:Put*, *:Modify* | Write actions |
Inline policies containing "Action": "*" or "Action": "<svc>:*" | Unbounded — assume write |
Validation logic
ALLOWED_PREFIXES = {"Describe", "Get", "List", "BatchGet"}
BLOCKED_PREFIXES = {
"Create", "Update", "Delete", "Put", "Modify", "Start", "Stop",
"Terminate", "Reboot", "Run", "Invoke", "Execute", "Send", "Publish",
"Tag", "Untag", "Attach", "Detach", "Associate", "Disassociate",
}
def is_read_only(actions: list[str]) -> bool:
for action in actions:
verb = action.split(":", 1)[1] if ":" in action else action
if verb == "*":
return False
if any(verb.startswith(p) for p in BLOCKED_PREFIXES):
return False
return TrueHow to check in practice
1. aws sts get-caller-identity — record the principal ARN. 2. For an IAM user: aws iam list-attached-user-policies + aws iam list-user-policies (inline). 3. For an IAM role: aws iam list-attached-role-policies + aws iam list-role-policies (inline). 4. For each policy ARN, aws iam get-policy-version → scan the Action list against BLOCKED_PREFIXES. 5. If any blocked action or * is present → HALT.
Boundary violation message
🚨 PERMISSION BOUNDARY VIOLATION
Your credentials ({arn}) have write permissions that exceed the read-only
boundary required for this FERPA assessment.
Detected write-capable policies:
{policy_list}
The assessment CANNOT proceed because:
• Write permissions could accidentally modify a system containing student
education records — itself a §99.32 misdisclosure risk
• Most state EdTech DPAs prohibit non-break-glass write access to production
environments containing student data
• This assessment is designed to be 100% non-destructive
ACTION REQUIRED:
1. Create an IAM role with ReadOnlyAccess or SecurityAudit policy
2. Assume that role (or configure new credentials for it)
3. Re-run the assessment
Example:
aws iam create-role --role-name FERPAAssessmentReadOnly \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy --role-name FERPAAssessmentReadOnly \
--policy-arn arn:aws:iam::aws:policy/SecurityAuditExceptions — metadata-only actions that look like writes
These are explicitly allowed because they produce reports/identity info without changing infrastructure:
sts:GetCallerIdentity— identity verificationsts:GetSessionToken/sts:AssumeRole— session managementiam:GenerateCredentialReport— generates an IAM report, does not modify IAMiam:GenerateServiceLastAccessedDetails— analysis job, no mutation
Region residency signal
FERPA itself does not require US region residency. Many state EdTech DPAs do (CA SOPIPA, NY Ed Law 2-d, TX SB 820, and most state-specific school-vendor contract templates). During Phase 1, after validating read-only permissions:
- If the caller's STS endpoint and enumerated resources are in non-US regions → flag as a potential state-contract gap (not a federal FERPA finding). Do not HALT — surface it in the bootstrap summary and carry it into the Phase 2 findings.
- If the user is explicitly operating under a state framework requiring FIPS endpoints (StateRAMP, TX-RAMP L2) → recommend setting
AWS_USE_FIPS_ENDPOINT=trueand using<service>-fips.<region>.amazonaws.comendpoints for the scan itself.
If the user insists on running with write credentials
Do not proceed. Explain the risk and offer two alternatives:
1. Questionnaire mode — no automated checks; walk the user through the readiness checklist manually. 2. Give the user the commands — emit the full list of aws CLI commands the assessment would have run, so they can execute them in a separate shell with the correct read-only credentials and paste results back.
FCD 3 — Disclosure Controls & Data Sharing — Programmatic Checks
Based on 34 CFR §99.31–§99.33, PTAC guidance. Last verified: 2026-05-21.
Execute in order. Read-only AWS CLI. Severity per `../severity-classification.md`.
FCD 3 covers the §99.31(a)(1)(i)(B) "school official" exception operationally: who can access student records, which external principals are receiving them, and whether those flows are authorized.
Most FCD 3 findings overlap with FCD 5 (access control) — the distinction is that FCD 3 is specifically about cross-boundary sharing (cross-account, cross-region, external subprocessors) rather than internal least-privilege.
---
FCD3-01: Active IAM Access Analyzer with external-access findings
aws accessanalyzer list-analyzers --query 'analyzers[?type==`ACCOUNT` && status==`ACTIVE`].arn' --output text
# For each analyzer, list active findings:
aws accessanalyzer list-findings --analyzer-arn {arn} --filter '{"status":{"eq":["ACTIVE"]},"isPublic":{"eq":["false"]}}' --output jsonWalk each finding — for each external-principal share, classify:
- Is the external principal a known subprocessor? (user must confirm from their DPA subprocessor register)
- Is it a district/school AWS account? Expected for FCD 3.
- Is it an unknown AWS account? Investigation required.
| Result | Severity | Finding |
|---|---|---|
| No Access Analyzer | BREACH RISK | Access Analyzer not enabled — cannot detect unauthorized cross-account disclosures |
| Findings for unknown external principals | BREACH RISK | {N} external-principal findings — review for unauthorized disclosures (potential §99.33 redisclosure violation) |
| Findings only for declared subprocessors/districts | INFO | External sharing limited to declared parties ✅ |
---
FCD3-02: RAM (Resource Access Manager) shares enumeration
aws ram get-resource-shares --resource-owner SELF --query 'resourceShares[].{Name:name,Status:status,Arn:resourceShareArn}' --output json
aws ram list-principals --resource-owner SELF --query 'principals[].{Share:resourceShareArn,Id:id,ResourceShareArn:resourceShareArn}' --output json| Result | Severity | Finding |
|---|---|---|
| Active shares to accounts not declared as subprocessors/districts | BREACH RISK | RAM share {name} to unknown principal — potential unauthorized disclosure |
| Shares align with declared subprocessor list | INFO | RAM shares within declared disclosure envelope ✅ |
| No RAM shares | NOT_APPLICABLE | — |
---
FCD3-03: Lake Formation cross-account grants
aws lakeformation list-permissions --query 'PrincipalResourcePermissions[?contains(keys(@), `Principal`)]' --output json 2>/dev/nullFlag any grant where the principal is a cross-account ARN not matching the caller's account.
| Result | Severity | Finding |
|---|---|---|
| Cross-account Lake Formation grants to unknown principals | BREACH RISK | Lake Formation grant to {principal} — review for authorized disclosure |
| Grants only to same-account or declared subprocessors | INFO | Lake Formation scoped ✅ |
| Lake Formation not in use | NOT_APPLICABLE | — |
---
FCD3-04: KMS key policies allowing cross-account use
aws kms list-keys --query 'Keys[].KeyId' --output text | tr '\t' '\n' | while read k; do
meta=$(aws kms describe-key --key-id "$k" --query 'KeyMetadata.{Manager:KeyManager,State:KeyState}' --output json 2>/dev/null)
if echo "$meta" | grep -q '"KeyManager": "CUSTOMER"'; then
pol=$(aws kms get-key-policy --key-id "$k" --policy-name default --query 'Policy' --output text 2>/dev/null)
# Look for external AWS account principals:
echo "$pol" | python3 -c "
import json, sys
p = json.loads(sys.stdin.read())
for stmt in p.get('Statement', []):
pr = stmt.get('Principal', {})
if isinstance(pr, dict):
aws = pr.get('AWS', [])
if isinstance(aws, str): aws = [aws]
for a in aws:
if 'arn:aws:iam::' in a and not a.startswith('arn:aws:iam::{account}:'):
print(f'Cross-account principal: {a}')
"
fi
done| Result | Severity | Finding |
|---|---|---|
| KMS keys with cross-account grants to unknown accounts | BREACH RISK | CMK {id} grants use to {principal} — potential unauthorized decryption of student records |
| Cross-account grants only to declared accounts | INFO | KMS grants within declared disclosure envelope ✅ |
| No cross-account grants | INFO | All KMS keys account-local ✅ |
---
FCD3-05: S3 bucket policies with non-local principals
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
pol=$(aws s3api get-bucket-policy --bucket "$b" --query 'Policy' --output text 2>/dev/null)
[ -z "$pol" ] && continue
echo "$pol" | python3 -c "
import json, sys
try:
p = json.loads(sys.stdin.read())
for stmt in p.get('Statement', []):
if stmt.get('Effect') != 'Allow': continue
pr = stmt.get('Principal', {})
if pr == '*' or (isinstance(pr, dict) and pr.get('AWS') == '*'):
print('WILDCARD')
elif isinstance(pr, dict):
aws = pr.get('AWS', [])
if isinstance(aws, str): aws = [aws]
for a in aws:
if 'arn:aws:iam::' in a:
print(f'Cross-account: {a}')
except: pass
"
done| Result | Severity | Finding |
|---|---|---|
Bucket policy with Principal: * | BREACH RISK | S3 bucket {name} has wildcard principal — public share |
| Bucket policy with cross-account principal not in declared subprocessor list | BREACH RISK | S3 bucket {name} shared with {principal} — unauthorized disclosure risk |
| All bucket policies local or to declared parties | INFO | S3 sharing scoped ✅ |
---
FCD3-06: VPC peering / Transit Gateway connections to unknown accounts
aws ec2 describe-vpc-peering-connections --query 'VpcPeeringConnections[?Status.Code==`active`].{Id:VpcPeeringConnectionId,Accepter:AccepterVpcInfo.OwnerId,Requester:RequesterVpcInfo.OwnerId}' --output json
aws ec2 describe-transit-gateway-attachments --query 'TransitGatewayAttachments[?State==`available`].{Id:TransitGatewayAttachmentId,ResourceOwner:ResourceOwnerId,ResourceType:ResourceType}' --output json| Result | Severity | Finding |
|---|---|---|
| Peering / TGW attachment to unknown account | COMPLIANCE GAP | VPC peering to account {id} — verify this is a declared subprocessor/district |
| All peerings to declared accounts | INFO | VPC peering scoped ✅ |
| None | NOT_APPLICABLE | — |
---
FCD3-07: VPC endpoints minimize public-network exposure
aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].{Id:VpcEndpointId,Service:ServiceName,Type:VpcEndpointType,Vpc:VpcId}' --output jsonExpected endpoints for a student-data VPC (minimum): s3, kms, logs, ssm, secretsmanager.
| Result | Severity | Finding |
|---|---|---|
| Student-data VPC without core endpoints (S3, KMS at minimum) | COMPLIANCE GAP | VPC {id} lacks core endpoints — AWS API traffic may traverse the public internet |
| Core endpoints present | INFO | VPC endpoints minimize public-network egress ✅ |
---
FCD3-08: Macie classification coverage on student-data buckets
aws macie2 get-macie-session --query 'status' --output text 2>/dev/null
aws macie2 list-classification-jobs --query 'items[].{Name:name,Status:jobStatus,Created:createdAt}' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| Macie disabled | COMPLIANCE GAP | Macie not enabled — cannot detect accidental student-PII disclosure (e.g., PII leaked into a non-student-data bucket) |
| Macie enabled but no recent classification jobs | HARDENING GAP | Macie enabled but no recent jobs — schedule periodic scans on student-data buckets |
| Macie scanning student-data buckets | INFO | Macie providing PII detection ✅ |
---
FCD3-09: Secrets Manager resource policies with cross-account principals
aws secretsmanager list-secrets --query 'SecretList[].ARN' --output text | tr '\t' '\n' | while read s; do
pol=$(aws secretsmanager get-resource-policy --secret-id "$s" --query 'ResourcePolicy' --output text 2>/dev/null)
[ -z "$pol" ] && continue
# Check for cross-account or * principals
echo "$pol" | python3 -c "
import json, sys
p = json.loads(sys.stdin.read())
for stmt in p.get('Statement', []):
if stmt.get('Effect') != 'Allow': continue
pr = stmt.get('Principal', {})
if pr == '*' or (isinstance(pr, dict) and pr.get('AWS') == '*'):
print('WILDCARD')
"
done| Result | Severity | Finding |
|---|---|---|
| Secrets Manager secret with wildcard principal | BREACH RISK | Secret {name} has wildcard resource policy — potential credential leak |
| All secrets scoped | INFO | Secrets scoped ✅ |
| No custom resource policies | NOT_APPLICABLE | — |
---
FCD3-10: SageMaker / Bedrock jobs reading student-data S3 (subprocessor check)
If ML/AI is used on student data, each job represents a potential subprocessor data flow.
# SageMaker training jobs referencing student-data buckets
aws sagemaker list-training-jobs --max-results 50 --query 'TrainingJobSummaries[].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}' --output json 2>/dev/null
# For each, describe and inspect InputDataConfig S3Uri:
aws sagemaker describe-training-job --training-job-name {name} --query 'InputDataConfig[].DataSource.S3DataSource.S3Uri' --output text 2>/dev/null
# Bedrock model-customization jobs (if applicable)
aws bedrock list-model-customization-jobs --max-results 50 --query 'modelCustomizationJobSummaries[].{Name:jobName,Status:status,Model:baseModelArn}' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| Training jobs reading student-data buckets without documented DPA authorization | BREACH RISK | Training job {name} reads from student-data — requires explicit district authorization under §99.33; model may be a redisclosure vehicle |
| All training jobs on non-student data | INFO | No ML training on student data detected ✅ |
| Training jobs with documented authorization | INFO | ML training on student data documented in DPA ✅ (manual check required) |
This check flags the pattern; the user must confirm DPA authorization. Model-training on student data without explicit district sign-off is a hot compliance topic and a frequent cause of state-AG investigations.
---
Summary
| Check | ID | Key question |
|---|---|---|
| Access Analyzer external findings | FCD3-01 | Are unauthorized external shares detected? |
| RAM shares | FCD3-02 | Are RAM shares to declared parties only? |
| Lake Formation grants | FCD3-03 | Are data-catalog shares scoped? |
| KMS cross-account | FCD3-04 | Can external accounts decrypt student data? |
| S3 cross-account | FCD3-05 | Are buckets shared only with declared parties? |
| VPC peering | FCD3-06 | Are network connections scoped? |
| VPC endpoints | FCD3-07 | Does AWS traffic stay private? |
| Macie coverage | FCD3-08 | Is PII leakage detection in place? |
| Secrets policies | FCD3-09 | Are secrets scoped? |
| ML/AI on student data | FCD3-10 | Is ML training authorized under DPA? |
Total: 10 checks. Expected time: ~3 min. FCD3-10 is the question most likely to surprise the user — always verify model-training authorization.
FCD 4 — Auditing & Access Logging — Programmatic Checks
Based on 34 CFR §99.32, PTAC guidance, NIST SP 800-171 §3.3. Last verified: 2026-05-21.
Execute in order. Each check uses read-only AWS CLI. Record results as
COMPLIANT/NON_COMPLIANT/NOT_APPLICABLE/UNABLE_TO_ASSESSwith severity per
`../severity-classification.md`.
FERPA FCD 4 covers both:
- §99.32 Record of disclosures — statutory requirement to log every request for access and every disclosure of education records. Application-level. CloudTrail does NOT satisfy this.
- PTAC / NIST 800-171 audit logging — security-event logging (auth, privilege changes, resource access). Infrastructure-level. CloudTrail + VPC Flow Logs + CloudWatch Logs satisfy this.
Key distinction from CJIS: the §99.32 log is a statutory requirement, not a best practice. Its absence is a BREACH RISK by default.
---
FCD4-01: CloudTrail enabled in all regions
aws cloudtrail describe-trails --query 'trailList[].{Name:Name,IsMultiRegion:IsMultiRegionTrail,IsOrgTrail:IsOrganizationTrail,S3Bucket:S3BucketName,KmsKey:KmsKeyId}' --output json| Result | Severity | Finding |
|---|---|---|
| No trails found | BREACH RISK | CloudTrail not configured — no API audit logging; undermines every other FCD 4 check |
| Trails exist but none multi-region | COMPLIANCE GAP | CloudTrail not multi-region — blind spots in other regions |
| Multi-region trail exists | — | Proceed to FCD4-02 |
---
FCD4-02: CloudTrail is actively logging
For each trail from FCD4-01:
aws cloudtrail get-trail-status --name {trail_arn_or_name} --query '{IsLogging:IsLogging,LatestDeliveryTime:LatestDeliveryTime,LatestDeliveryError:LatestDeliveryError}' --output json| Result | Severity | Finding |
|---|---|---|
IsLogging: false | BREACH RISK | CloudTrail trail {name} exists but logging is stopped |
| Delivery error present | COMPLIANCE GAP | CloudTrail delivery errors — logs may be incomplete |
| Logging, no errors | INFO | CloudTrail actively logging ✅ |
---
FCD4-03: CloudTrail log file validation enabled
aws cloudtrail describe-trails --query 'trailList[].{Name:Name,LogFileValidation:LogFileValidationEnabled}' --output json| Result | Severity | Finding |
|---|---|---|
LogFileValidation: false | COMPLIANCE GAP | Log file validation disabled — cannot detect tampering |
LogFileValidation: true | INFO | Log integrity protection enabled ✅ |
---
FCD4-04: CloudTrail logs encrypted with KMS CMK
aws cloudtrail describe-trails --query 'trailList[].{Name:Name,KmsKeyId:KmsKeyId}' --output json| Result | Severity | Finding |
|---|---|---|
KmsKeyId: null | COMPLIANCE GAP | CloudTrail logs not encrypted with CMK (SSE-S3 only) |
| KMS key ID present | INFO | CloudTrail logs KMS-encrypted ✅ |
---
FCD4-05: CloudTrail data events for S3 buckets containing student records
aws cloudtrail get-event-selectors --trail-name {trail_arn} --query 'EventSelectors[].{ReadWrite:ReadWriteType,DataResources:DataResources}' --output json
# Or for advanced event selectors:
aws cloudtrail get-event-selectors --trail-name {trail_arn} --query 'AdvancedEventSelectors' --output json| Result | Severity | Finding |
|---|---|---|
| No S3 data events configured | COMPLIANCE GAP | S3 object-level access to student-record buckets is not logged |
| S3 data events for "all buckets" | INFO | S3 data events captured ✅ |
| Data events limited to named buckets | INFO | Confirm the listed buckets cover all student-data buckets declared in Phase 1 (manual check) |
Data events are billed, so "all buckets" may be prohibitive. If scoped, the user must confirm student-data bucket coverage — surface as a questionnaire item.
---
FCD4-06: CloudTrail data events for Lambda functions that read student-data S3
aws cloudtrail get-event-selectors --trail-name {trail_arn} --output json
# Look for Lambda::Function data resources in the output| Result | Severity | Finding |
|---|---|---|
| No Lambda data events | HARDENING GAP | Lambda functions processing student data are not logged at invocation level |
| Lambda data events present | INFO | Lambda invocation logging enabled ✅ |
---
FCD4-07: VPC Flow Logs enabled on all VPCs
aws ec2 describe-vpcs --query 'Vpcs[].VpcId' --output text | tr '\t' '\n' | while read vpc; do
logs=$(aws ec2 describe-flow-logs --filter Name=resource-id,Values=$vpc --query 'FlowLogs[?FlowLogStatus==`ACTIVE`].FlowLogId' --output text)
[ -z "$logs" ] && echo "$vpc: NO ACTIVE FLOW LOGS"
done| Result | Severity | Finding |
|---|---|---|
| Any VPC without active flow logs | COMPLIANCE GAP | VPC {id} has no flow logs — limited network forensics |
| All VPCs have flow logs | INFO | VPC flow logging complete ✅ |
---
FCD4-08: CloudTrail log bucket has S3 Object Lock (tamper protection)
# Identify the bucket from FCD4-01 output
aws s3api get-object-lock-configuration --bucket {log-bucket} --query 'ObjectLockConfiguration' --output json 2>/dev/null
aws s3api get-bucket-versioning --bucket {log-bucket} --query '{Status:Status,MFADelete:MFADelete}' --output json| Result | Severity | Finding |
|---|---|---|
| Object Lock not enabled AND versioning disabled | COMPLIANCE GAP | CloudTrail log bucket lacks tamper protection — logs could be deleted |
| Versioning enabled, Object Lock disabled | HARDENING GAP | Consider enabling Object Lock (Governance or Compliance mode) for stronger tamper protection |
| Object Lock enabled | INFO | Log tamper protection in place ✅ |
---
FCD4-09: Log retention meets student-record retention schedule
For CloudWatch Logs groups:
aws logs describe-log-groups --query 'logGroups[].{Name:logGroupName,Retention:retentionInDays}' --output jsonFor CloudTrail S3 bucket, inspect lifecycle:
aws s3api get-bucket-lifecycle-configuration --bucket {log-bucket} --query 'Rules[].{ID:ID,Status:Status,Expiration:Expiration}' --output json 2>/dev/nullExpected floor for student-data-related log groups: ≥2555 days (7 years) or null (never expire). State schedules vary — some require 5 years, some ≥10 years for certain record categories.
| Result | Severity | Finding |
|---|---|---|
Any student-data log group with retention < 2555 days (not null) | COMPLIANCE GAP | Log group {name} retention is {N} days — below typical 7-year student-record floor |
| CloudTrail S3 lifecycle expires logs before retention schedule | COMPLIANCE GAP | CloudTrail log bucket lifecycle expires logs at {N} days |
| Retention ≥ schedule or never-expire | INFO | Log retention meets student-record schedule ✅ |
Note: this check is heuristic — the user declared retention requirements in Phase 1 may differ. Surface the raw retention values and let the user confirm.
---
FCD4-10: §99.32 disclosure log — application-level (heuristic)
CloudTrail cannot prove the §99.32 log exists — the application emits it. Heuristic check: look for a dedicated CloudWatch log group with a naming convention suggesting a disclosure log, and confirm it has recent entries.
# Heuristic: log groups containing "disclosure", "access-log", "ferpa", "99-32", "access-record"
aws logs describe-log-groups --log-group-name-prefix "/ferpa/" --query 'logGroups[].logGroupName' --output json 2>/dev/null
aws logs describe-log-groups --log-group-name-prefix "/disclosure/" --query 'logGroups[].logGroupName' --output json 2>/dev/null
aws logs describe-log-groups --log-group-name-prefix "/student-access/" --query 'logGroups[].logGroupName' --output json 2>/dev/null
# If any match, check for recent activity:
aws logs describe-log-streams --log-group-name {name} --order-by LastEventTime --descending --limit 1 --query 'logStreams[0].lastEventTimestamp' --output text| Result | Severity | Finding |
|---|---|---|
| No candidate log group AND application handles student records | BREACH RISK | No §99.32 disclosure log detected — FERPA statutory requirement not met (application must emit structured disclosure records) |
| Candidate log group exists but no activity in >30 days | COMPLIANCE GAP | §99.32 disclosure log may be stale — confirm the application is still emitting events |
| Candidate log group with recent activity | INFO | §99.32 disclosure log present ✅ (manual confirmation recommended — check log structure includes student_id, requestor, purpose, exception, records) |
This is the single most important FERPA-specific check. Even if automated detection fails, surface it as a MANDATORY questionnaire item: "Does your application log every disclosure of student records with {timestamp, student_id, requestor, purpose, §99.31 exception, records[]}?" Capture the user's yes/no answer in the report.
---
FCD4-11: CloudWatch alarms for critical security events
Expected alarms: root login, MFA disable, IAM policy changes, S3 bucket policy changes, CloudTrail config changes.
aws cloudwatch describe-alarms --query 'MetricAlarms[].AlarmName' --output jsonHeuristic: look for alarms matching patterns like root, mfa, iam, cloudtrail.
| Result | Severity | Finding |
|---|---|---|
| Fewer than 3 security alarms configured | HARDENING GAP | Limited proactive alerting — consider CIS AWS Foundations Benchmark alarms |
| Reasonable alarm coverage | INFO | Security alarms in place ✅ |
---
FCD4-12: GuardDuty enabled
aws guardduty list-detectors --query 'DetectorIds' --output json
# If present, check status:
aws guardduty get-detector --detector-id {id} --query '{Status:Status,DataSources:DataSources}' --output json| Result | Severity | Finding |
|---|---|---|
| No detector | COMPLIANCE GAP | GuardDuty not enabled — no automated threat detection on student-data accounts |
Detector exists, Status: DISABLED | COMPLIANCE GAP | GuardDuty disabled |
| Detector enabled, S3 protection on | INFO | GuardDuty protecting student-data S3 ✅ |
| Detector enabled, S3 protection off | HARDENING GAP | Enable GuardDuty S3 protection for student-data buckets |
---
Summary
| Check | ID | Key question |
|---|---|---|
| CloudTrail enabled | FCD4-01 | Is there any API audit logging? |
| CloudTrail logging | FCD4-02 | Are the trails actually delivering? |
| Log validation | FCD4-03 | Can we detect log tampering? |
| Log encryption | FCD4-04 | Are logs KMS-encrypted? |
| S3 data events | FCD4-05 | Is student-bucket access logged? |
| Lambda data events | FCD4-06 | Are student-data Lambdas logged? |
| VPC Flow Logs | FCD4-07 | Is network access logged? |
| Log tamper protection | FCD4-08 | Are logs deletion-protected? |
| Log retention | FCD4-09 | Do logs meet the retention schedule? |
| §99.32 disclosure log | FCD4-10 | Does the app log every student-record disclosure? |
| Security alarms | FCD4-11 | Is critical-event alerting in place? |
| GuardDuty | FCD4-12 | Is threat detection enabled? |
Total: 12 checks. Expected time: ~3 min. FCD4-10 is the most important — if nothing else, make sure the §99.32 question reaches the user.
FCD 5 — Access Control & Least Privilege — Programmatic Checks
Based on PTAC guidance, NIST SP 800-171 §3.1. Last verified: 2026-05-21.
Execute in order. Read-only AWS CLI. Severity per `../severity-classification.md`.
FCD 5 covers authorization and session controls — what an authenticated principal is allowed to do. FCD 6 covers authentication. Overlap is intentional.
Public-facing student-data resources (public S3, public RDS, public snapshots) are the breach vector #1 in state-AG EdTech breach notifications — treat as BREACH RISK.
---
FCD5-01: No overly-permissive IAM policies (*:* wildcards)
aws iam list-policies --scope Local --only-attached --query 'Policies[].{Arn:Arn,Name:PolicyName,DefaultVersion:DefaultVersionId}' --output json
# For each attached customer-managed policy:
# aws iam get-policy-version --policy-arn {arn} --version-id {ver}Scan each policy document for:
"Action": "*"with"Resource": "*"and"Effect": "Allow"→ unbounded admin"Action": "<svc>:*"on sensitive services (iam,kms,s3,rds,dynamodb) with"Resource": "*"→ service-wide admin
| Result | Severity | Finding |
|---|---|---|
Policy with Action: *, Resource: *, Effect: Allow, no conditions | BREACH RISK | Customer policy {name} grants unbounded admin |
Policy with <svc>:* on sensitive service, no resource scoping | COMPLIANCE GAP | Policy {name} broad on service {svc} |
| All policies scoped | INFO | No overly-permissive customer policies ✅ |
Managed policies like AdministratorAccess are expected to exist — flag only if attached to a principal that touches student data.
---
FCD5-02: IAM Access Analyzer findings for external access
aws accessanalyzer list-analyzers --query 'analyzers[?status==`ACTIVE`].{Name:name,Arn:arn,Type:type}' --output json
# For each active analyzer:
aws accessanalyzer list-findings --analyzer-arn {arn} --filter '{"status":{"eq":["ACTIVE"]}}' --query 'findings[].{Id:id,Resource:resource,ExternalPrincipal:principal}' --output json| Result | Severity | Finding |
|---|---|---|
| No Access Analyzer enabled | COMPLIANCE GAP | Access Analyzer not enabled — enable to catch cross-account/public exposure |
| Active findings for external principals | COMPLIANCE GAP | {count} resources exposed to external principals per Access Analyzer — review each for subprocessor/disclosure implications |
| No active findings | INFO | No external-principal exposure ✅ |
---
FCD5-03: No publicly-accessible RDS instances
aws rds describe-db-instances --query 'DBInstances[?PubliclyAccessible==`true`].{Id:DBInstanceIdentifier,Endpoint:Endpoint.Address,Engine:Engine}' --output json| Result | Severity | Finding |
|---|---|---|
| Any publicly accessible RDS | BREACH RISK | RDS {id} is publicly accessible — may expose student data |
| None public | INFO | RDS instances not publicly accessible ✅ |
---
FCD5-04: No publicly-shared RDS or EBS snapshots
aws rds describe-db-snapshots --snapshot-type manual --include-public --query 'DBSnapshots[?contains(keys(@), `SnapshotCreateTime`)] | [?not_null(@)].{Id:DBSnapshotIdentifier}' --output json
aws rds describe-db-snapshot-attributes --db-snapshot-identifier {id} --query 'DBSnapshotAttributesResult.DBSnapshotAttributes[?AttributeName==`restore`].AttributeValues' --output json
# Check for "all" in AttributeValues — means public share
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[].SnapshotId' --output text | tr '\t' '\n' | while read s; do
pub=$(aws ec2 describe-snapshot-attribute --snapshot-id "$s" --attribute createVolumePermission --query 'CreateVolumePermissions[?Group==`all`]' --output text 2>/dev/null)
[ -n "$pub" ] && echo "$s: PUBLIC"
done| Result | Severity | Finding |
|---|---|---|
Any RDS snapshot with restore: all | BREACH RISK | RDS snapshot {id} publicly shared — potential student-data disclosure |
| Any EBS snapshot publicly shared | BREACH RISK | EBS snapshot {id} publicly shared — revoke immediately |
| None public | INFO | No public snapshots ✅ |
---
FCD5-05: No public S3 bucket policies / ACLs
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
pol_status=$(aws s3api get-bucket-policy-status --bucket "$b" --query 'PolicyStatus.IsPublic' --output text 2>/dev/null)
[ "$pol_status" = "True" ] && echo "$b: PUBLIC POLICY"
acl=$(aws s3api get-bucket-acl --bucket "$b" --query 'Grants[?Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers` || Grantee.URI==`http://acs.amazonaws.com/groups/global/AuthenticatedUsers`].Permission' --output text 2>/dev/null)
[ -n "$acl" ] && echo "$b: PUBLIC ACL ($acl)"
done| Result | Severity | Finding |
|---|---|---|
| Any public buckets | BREACH RISK | S3 bucket {name} publicly accessible — highest-urgency FERPA finding |
| None public | INFO | No public S3 buckets ✅ |
---
FCD5-06: S3 Block Public Access enabled account-wide
aws s3control get-public-access-block --account-id {account} --query 'PublicAccessBlockConfiguration' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
NoSuchPublicAccessBlockConfiguration or any flag false | COMPLIANCE GAP | S3 BPA not fully enabled at account level — enable all four flags |
All four flags true | INFO | S3 BPA fully enabled ✅ |
---
FCD5-07: Remote access via Session Manager / encrypted channels (no direct SSH)
# Count SGs allowing 22 or 3389 from 0.0.0.0/0
aws ec2 describe-security-groups --filters Name=ip-permission.from-port,Values=22,3389 --query 'SecurityGroups[].{Id:GroupId,Ingress:IpPermissions[?FromPort==`22` || FromPort==`3389`].IpRanges[?CidrIp==`0.0.0.0/0`]}' --output json
# Check for recent SSM Session history
aws ssm describe-sessions --state History --max-results 10 --query 'Sessions[].SessionId' --output text 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| SGs allow 22/3389 from 0.0.0.0/0 | BREACH RISK | Security group {id} allows SSH/RDP from internet |
| SGs allow 22/3389 from private ranges AND no SSM session history | COMPLIANCE GAP | Direct SSH/RDP appears to be in use — adopt SSM Session Manager for audit trail |
| SSM Session Manager in use | INFO | Session Manager providing audited access ✅ |
---
FCD5-08: SCPs in place for student-data accounts (Organizations)
aws organizations list-policies --filter SERVICE_CONTROL_POLICY --query 'Policies[].{Id:Id,Name:Name,AwsManaged:AwsManaged}' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| Organizations not in use | HARDENING GAP | Single-account setup — SCPs not applicable; confirm student-data isolation via IAM only |
| Organizations in use, no custom SCPs | HARDENING GAP | Only AWS-managed SCPs — consider student-data-specific SCPs (US region restriction, deny-delete-logs, deny-non-approved-services) |
| Custom SCPs applied to student-data OU | INFO | Custom SCPs enforcing the student-data boundary ✅ |
---
FCD5-09: No inactive/stale IAM roles
aws iam list-roles --query 'Roles[?!starts_with(Path, `/aws-service-role/`) && !starts_with(Path, `/aws-reserved/`)].{Name:RoleName,Arn:Arn,LastUsed:RoleLastUsed.LastUsedDate}' --output json| Result | Severity | Finding |
|---|---|---|
Customer roles with LastUsed > 180 days or null | HARDENING GAP | {count} customer IAM roles appear unused — review and delete |
| All roles recently used | INFO | No stale roles ✅ |
---
FCD5-10: No IAM policies allowing iam:PassRole broadly
Dangerous pattern: iam:PassRole with Resource: * or without role-name condition enables privilege escalation.
aws iam list-policies --scope Local --only-attached --output json
# For each, fetch the default version and grep for iam:PassRole| Result | Severity | Finding |
|---|---|---|
Policy with iam:PassRole + Resource: *, no conditions | BREACH RISK | Policy {name} permits unscoped PassRole — privilege escalation risk |
Policy with iam:PassRole scoped to specific role ARNs | INFO | PassRole scoped ✅ |
| No PassRole in customer policies | NOT_APPLICABLE | — |
---
Summary
| Check | ID | Key question |
|---|---|---|
| Overly-permissive policies | FCD5-01 | Any unbounded admin policies? |
| Access Analyzer | FCD5-02 | Is external exposure monitored? |
| Public RDS | FCD5-03 | Are databases private? |
| Public snapshots | FCD5-04 | Are snapshots private? |
| Public S3 | FCD5-05 | Are buckets private? |
| S3 BPA | FCD5-06 | Is S3 Block Public Access on? |
| Audited shell access | FCD5-07 | Is direct SSH replaced by SSM? |
| SCPs | FCD5-08 | Are Org-level guardrails in place? |
| Stale roles | FCD5-09 | Are unused roles cleaned up? |
| PassRole scope | FCD5-10 | Is PassRole scoped to prevent escalation? |
Total: 10 checks. Expected time: ~3 min.
FCD 6 — Authentication — Programmatic Checks
Based on PTAC guidance, NIST SP 800-171 §3.5. Last verified: 2026-05-21.
Execute in order. Read-only AWS CLI. Severity per `../severity-classification.md`.
FERPA itself doesn't specify authentication requirements — "reasonable methods." The PTAC "Data Security Checklist," NIST SP 800-171 (3.5.x), and every modern state EdTech DPA establish the baseline:
- MFA for admin access and for any access that touches bulk student records
- Unique IDs — no shared accounts
- Password policy: length, complexity, ≤90-day age, lockout after 5 failed attempts
- Root account locked down
Credential compromise on unMFAed admin accounts is one of the top EdTech breach vectors — treat missing MFA as BREACH RISK.
---
FCD6-01: Root account MFA enabled
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled' --output text| Result | Severity | Finding |
|---|---|---|
0 | BREACH RISK | Root account MFA not enabled — single compromised credential compromises the entire environment |
1 | INFO | Root MFA enabled ✅ |
---
FCD6-02: Root account has no active access keys
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent' --output text| Result | Severity | Finding |
|---|---|---|
1 | BREACH RISK | Root access keys exist — must be deleted (AWS best practice + state-DPA baseline) |
0 | INFO | No root access keys ✅ |
---
FCD6-03: All IAM users with console access have MFA
aws iam generate-credential-report >/dev/null 2>&1; sleep 2
aws iam get-credential-report --query Content --output text | base64 -d > /tmp/cred-report.csv
# Columns: user, arn, user_creation_time, password_enabled, password_last_used,
# password_last_changed, password_next_rotation, mfa_active, ...
awk -F',' 'NR>1 && $4=="true" && $8=="false" {print $1}' /tmp/cred-report.csv| Result | Severity | Finding |
|---|---|---|
Any users with password_enabled=true, mfa_active=false | BREACH RISK | {count} IAM users have console access without MFA — credential-compromise risk to student data |
| All console users have MFA | INFO | MFA enforced on all IAM console users ✅ |
---
FCD6-04: IAM password policy meets student-data baseline
aws iam get-account-password-policy --output json 2>/dev/nullStudent-data baseline (from PTAC + state-DPA common denominators):
MinimumPasswordLength≥ 14 (some state DPAs accept 12; 14 is the strict modern benchmark)RequireSymbols,RequireNumbers,RequireUppercaseCharacters,RequireLowercaseCharactersalltrueMaxPasswordAge≤ 90PasswordReusePrevention≥ 10
| Result | Severity | Finding |
|---|---|---|
No policy set (NoSuchEntity) | BREACH RISK | No IAM password policy configured |
MinimumPasswordLength < 12 | BREACH RISK | Password minimum length below minimum baseline |
MinimumPasswordLength < 14 | COMPLIANCE GAP | Password length below strict benchmark (14 chars) |
| Missing any complexity flag | COMPLIANCE GAP | Password policy missing complexity requirements |
MaxPasswordAge > 90 or unset | COMPLIANCE GAP | Password age exceeds 90 days |
PasswordReusePrevention < 10 | HARDENING GAP | Password reuse prevention below recommended (10+) |
| All thresholds met | INFO | Password policy meets student-data baseline ✅ |
---
FCD6-05: Access key age ≤ 90 days
aws iam generate-credential-report >/dev/null 2>&1; sleep 2
aws iam get-credential-report --query Content --output text | base64 -d > /tmp/cred-report.csv
python3 - <<'PY'
import csv, datetime
now = datetime.datetime.utcnow()
over = []
with open('/tmp/cred-report.csv') as f:
for row in csv.DictReader(f):
for k in ('access_key_1_last_rotated', 'access_key_2_last_rotated'):
v = row.get(k, 'N/A')
if v in ('N/A', 'no_information', ''): continue
age = (now - datetime.datetime.strptime(v.split('+')[0].rstrip('Z'), '%Y-%m-%dT%H:%M:%S')).days
if age > 90:
over.append((row['user'], k, age))
for u, k, a in over:
print(f"{u}: {k} age {a}d")
PY| Result | Severity | Finding |
|---|---|---|
| Any key > 180 days | BREACH RISK | {count} access keys older than 180 days — rotate or delete immediately |
| Any key 90-180 days | COMPLIANCE GAP | {count} access keys over 90-day rotation baseline |
| All ≤ 90 days | INFO | Access keys within rotation policy ✅ |
---
FCD6-06: No shared / generic IAM users
aws iam list-users --query 'Users[].{UserName:UserName,Path:Path,CreateDate:CreateDate}' --output jsonHeuristic: flag user names that look generic (case-insensitive): admin, root, shared, service, ops, automation, jenkins, ci, cd, or names with no vowels/digits only.
| Result | Severity | Finding |
|---|---|---|
Any user names match the generic list AND have password_enabled=true | COMPLIANCE GAP | Possible shared IAM user: {name} — student-data baseline requires unique attributable accounts |
| No suspicious names | INFO | No obvious shared users detected ✅ |
Heuristic — always surface the list to the user for confirmation. A named service account (ci-deploy-prod) without console access is fine; a human-like shared console account is not.
---
FCD6-07: IAM Identity Center (SSO) configured
aws sso-admin list-instances --output json 2>/dev/null
# If an instance exists:
aws sso-admin describe-instance-access-control-attribute-configuration --instance-arn {arn} 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| No Identity Center instance, and multiple IAM users detected | COMPLIANCE GAP | Consider IAM Identity Center for centralized MFA + federation; long-lived IAM users are an anti-pattern for student-data environments |
| Identity Center instance exists | INFO | Identity Center in use — confirm MFA required (manual check: Console → Settings → Authentication → "Every time they sign in") |
| No Identity Center, single-user admin account | HARDENING GAP | Single-user setup — still acceptable but lacks federation advantages |
Identity Center's MFA enforcement setting isn't exposed cleanly via CLI. Mark as a follow-up: confirm in the console.
---
FCD6-08: No long-unused IAM users
# Reuse /tmp/cred-report.csv from FCD6-03
python3 - <<'PY'
import csv, datetime
now = datetime.datetime.utcnow()
stale = []
with open('/tmp/cred-report.csv') as f:
for row in csv.DictReader(f):
last = row.get('password_last_used', 'N/A')
if last in ('N/A', 'no_information', ''): continue
age = (now - datetime.datetime.strptime(last.split('+')[0].rstrip('Z'), '%Y-%m-%dT%H:%M:%S')).days
if age > 90:
stale.append((row['user'], age))
for u, a in stale:
print(f"{u}: last login {a}d ago")
PY| Result | Severity | Finding |
|---|---|---|
| Users with login > 90 days | COMPLIANCE GAP | {count} IAM users inactive > 90 days — disable or delete (FCD 5 + FCD 6) |
| All active | INFO | No stale console users ✅ |
---
FCD6-09: Cognito user pools enforce MFA (end-user auth)
If the application uses Cognito for student/parent/teacher authentication:
aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].{Id:Id,Name:Name}' --output json
# For each pool:
aws cognito-idp describe-user-pool --user-pool-id {id} --query 'UserPool.{MfaConfiguration:MfaConfiguration,Policies:Policies.PasswordPolicy}' --output json| Result | Severity | Finding |
|---|---|---|
Pool with MfaConfiguration: OFF serving admin-class users | BREACH RISK | Cognito pool {name} has MFA disabled — admin auth path unprotected |
Pool MfaConfiguration: OPTIONAL | COMPLIANCE GAP | MFA optional — enforce for admin/staff roles |
Pool MfaConfiguration: ON | INFO | MFA enforced on Cognito pool ✅ |
| No Cognito pools | NOT_APPLICABLE | End-user auth not via Cognito |
Note: for student-facing pools (K-12 under-13), MFA may be impractical — surface as a design question: "Is this pool used by students under 13? If so, ensure SSO federation from the district IdP handles auth upstream."
---
Summary
| Check | ID | Key question |
|---|---|---|
| Root MFA | FCD6-01 | Is root protected with MFA? |
| Root access keys | FCD6-02 | Are root API keys eliminated? |
| User MFA | FCD6-03 | Do all console users have MFA? |
| Password policy | FCD6-04 | Does the policy meet the baseline? |
| Key rotation | FCD6-05 | Are access keys rotated ≤90 days? |
| Shared accounts | FCD6-06 | Are all accounts uniquely attributable? |
| Identity Center | FCD6-07 | Is federated SSO + MFA in use? |
| Stale users | FCD6-08 | Are inactive accounts disabled? |
| Cognito MFA | FCD6-09 | Is end-user auth MFA-protected? |
Total: 9 checks. Expected time: ~2 min.
FCD 7 — Encryption at Rest & In Transit — Programmatic Checks
Based on PTAC guidance, NIST SP 800-171 §3.13. Last verified: 2026-05-21.
Execute in order. Read-only AWS CLI. Severity per `../severity-classification.md`.
FERPA doesn't mandate encryption. PTAC guidance and modern state EdTech DPAs do — unencrypted S3 buckets exposed to the internet are the most common root cause in state-AG breach notifications for EdTech vendors.
Retrofit-encryption limitation: RDS, Aurora, OpenSearch, and ElastiCache cannot be encrypted after creation. Unencrypted production data stores require snapshot-restore migration.
---
FCD7-01: Default EBS encryption enabled at account level
aws ec2 get-ebs-encryption-by-default --query 'EbsEncryptionByDefault' --output text
aws ec2 get-ebs-default-kms-key-id --query 'KmsKeyId' --output text| Result | Severity | Finding |
|---|---|---|
EbsEncryptionByDefault: false | COMPLIANCE GAP | Default EBS encryption disabled — new volumes may be unencrypted |
Enabled, default key is AWS-managed (alias/aws/ebs) | HARDENING GAP | Default key is AWS-managed — use a CMK for auditability |
| Enabled with CMK | INFO | Default EBS encryption with CMK ✅ |
---
FCD7-02: All existing EBS volumes are encrypted
aws ec2 describe-volumes --filters Name=encrypted,Values=false --query 'Volumes[].{Id:VolumeId,Size:Size,State:State}' --output json| Result | Severity | Finding |
|---|---|---|
Any Encrypted=false volumes | BREACH RISK | {count} unencrypted EBS volumes — migrate via snapshot (create snapshot, copy-with-encryption, create new volume) |
| All encrypted | INFO | All EBS volumes encrypted ✅ |
---
FCD7-03: All RDS instances encrypted at rest
aws rds describe-db-instances --query 'DBInstances[?StorageEncrypted==`false`].{Id:DBInstanceIdentifier,Engine:Engine,Status:DBInstanceStatus}' --output json| Result | Severity | Finding |
|---|---|---|
| Any unencrypted RDS | BREACH RISK | RDS instance {id} unencrypted — cannot retrofit; must migrate via snapshot-restore |
| All encrypted | INFO | All RDS instances encrypted ✅ |
---
FCD7-04: All RDS snapshots encrypted
aws rds describe-db-snapshots --snapshot-type manual --query 'DBSnapshots[?Encrypted==`false`].{Id:DBSnapshotIdentifier}' --output json
aws rds describe-db-snapshots --snapshot-type automated --query 'DBSnapshots[?Encrypted==`false`].{Id:DBSnapshotIdentifier}' --output json| Result | Severity | Finding |
|---|---|---|
| Unencrypted snapshots exist | BREACH RISK | Unencrypted RDS snapshots present — copy with encryption and delete originals |
| All encrypted | INFO | All RDS snapshots encrypted ✅ |
---
FCD7-05: All S3 buckets have default encryption
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
enc=$(aws s3api get-bucket-encryption --bucket "$b" --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.SSEAlgorithm' --output text 2>/dev/null)
[ -z "$enc" ] || [ "$enc" = "None" ] && echo "$b: NO DEFAULT ENCRYPTION"
doneNote: as of Jan 2023, AWS enables SSE-S3 by default on new buckets. Older buckets or explicitly-configured ones may lack it.
| Result | Severity | Finding |
|---|---|---|
| Any bucket without default encryption | COMPLIANCE GAP | S3 bucket {name} lacks default encryption — configure SSE-KMS |
| All buckets encrypted with SSE-S3 only | HARDENING GAP | Buckets use SSE-S3 — upgrade to SSE-KMS with CMK for auditability |
| All buckets SSE-KMS | INFO | All S3 buckets SSE-KMS encrypted ✅ |
---
FCD7-06: S3 buckets deny unencrypted transport (HTTP)
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
pol=$(aws s3api get-bucket-policy --bucket "$b" --query 'Policy' --output text 2>/dev/null)
if [ -z "$pol" ] || ! echo "$pol" | grep -q "aws:SecureTransport"; then
echo "$b: NO SECURE-TRANSPORT DENY"
fi
done| Result | Severity | Finding |
|---|---|---|
Buckets without aws:SecureTransport: false deny | COMPLIANCE GAP | S3 bucket {name} accepts plain HTTP — add deny policy |
| All buckets enforce TLS | INFO | All S3 buckets enforce TLS ✅ |
---
FCD7-07: DynamoDB encryption uses CMK for student-data tables
aws dynamodb list-tables --query 'TableNames' --output text | tr '\t' '\n' | while read t; do
enc=$(aws dynamodb describe-table --table-name "$t" --query 'Table.SSEDescription.{Type:SSEType,Key:KMSMasterKeyArn}' --output json 2>/dev/null)
echo "$t: $enc"
doneAll DynamoDB tables are encrypted by default (AWS-owned key). The check is for CMK usage on student-data tables.
| Result | Severity | Finding |
|---|---|---|
Student-data table using AWS-owned key (no SSEDescription) | HARDENING GAP | Table {name} uses AWS-owned key — switch to CMK for auditability |
Using CMK (KMS type) | INFO | Table uses CMK ✅ |
Student-data tables should be identified from the Phase 1 scope declaration.
---
FCD7-08: KMS CMKs have automatic rotation enabled
aws kms list-keys --query 'Keys[].KeyId' --output text | tr '\t' '\n' | while read k; do
meta=$(aws kms describe-key --key-id "$k" --query 'KeyMetadata.{Origin:Origin,Manager:KeyManager,State:KeyState}' --output json 2>/dev/null)
if echo "$meta" | grep -q '"KeyManager": "CUSTOMER"'; then
rot=$(aws kms get-key-rotation-status --key-id "$k" --query 'KeyRotationEnabled' --output text 2>/dev/null)
[ "$rot" != "True" ] && echo "$k: ROTATION OFF"
fi
done| Result | Severity | Finding |
|---|---|---|
| Customer CMKs with rotation disabled | COMPLIANCE GAP | {count} CMKs lack automatic rotation — enable via console or aws kms enable-key-rotation |
| All CMKs rotating | INFO | KMS key rotation enabled ✅ |
---
FCD7-09: ALB / NLB listeners use TLS 1.2+
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerArn' --output text | tr '\t' '\n' | while read lb; do
aws elbv2 describe-listeners --load-balancer-arn "$lb" --query 'Listeners[?Protocol==`HTTPS` || Protocol==`TLS`].{LB:LoadBalancerArn,Port:Port,SslPolicy:SslPolicy}' --output json
doneFlag listeners whose SSL policy is older than ELBSecurityPolicy-TLS-1-2-2017-01 or includes TLS 1.0/1.1 (any policy containing TLS-1-0 or 2015 or 2016).
| Result | Severity | Finding |
|---|---|---|
| Listener with TLS 1.0/1.1-inclusive policy | COMPLIANCE GAP | Listener {arn}:{port} permits deprecated TLS — upgrade to ELBSecurityPolicy-TLS13-1-2-2021-06 |
| Listener with HTTP (non-TLS) accepting student-data traffic | BREACH RISK | Listener {arn}:{port} is HTTP — student data in plaintext |
| All TLS 1.2+ | INFO | Load balancer TLS policies current ✅ |
---
FCD7-10: CloudFront distributions enforce HTTPS
aws cloudfront list-distributions --query 'DistributionList.Items[].{Id:Id,Aliases:Aliases.Items,Viewer:DefaultCacheBehavior.ViewerProtocolPolicy,MinTLS:ViewerCertificate.MinimumProtocolVersion}' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
Any ViewerProtocolPolicy: allow-all | COMPLIANCE GAP | Distribution {id} allows HTTP — change to redirect-to-https or https-only |
MinimumProtocolVersion below TLSv1.2_2021 | COMPLIANCE GAP | Distribution {id} allows TLS < 1.2 — raise minimum to TLSv1.2_2021 |
| All distributions TLS 1.2+, HTTPS-only | INFO | CloudFront TLS policies current ✅ |
| No distributions | NOT_APPLICABLE | — |
---
FCD7-11: RDS force SSL (parameter-group check)
aws rds describe-db-instances --query 'DBInstances[].{Id:DBInstanceIdentifier,Engine:Engine,PG:DBParameterGroups[0].DBParameterGroupName}' --output json
# For each parameter group:
aws rds describe-db-parameters --db-parameter-group-name {pg} --query 'Parameters[?ParameterName==`require_secure_transport` || ParameterName==`rds.force_ssl`].{Name:ParameterName,Value:ParameterValue}' --output json| Result | Severity | Finding |
|---|---|---|
MySQL/MariaDB with require_secure_transport != 1 | COMPLIANCE GAP | RDS {id} does not require SSL — update parameter group |
Postgres with rds.force_ssl != 1 | COMPLIANCE GAP | RDS {id} does not require SSL — update parameter group |
| All instances enforce SSL | INFO | RDS force-SSL enabled ✅ |
---
FCD7-12: OpenSearch encryption (at-rest and node-to-node)
aws opensearch list-domain-names --query 'DomainNames[].DomainName' --output text | tr '\t' '\n' | while read d; do
aws opensearch describe-domain --domain-name "$d" --query 'DomainStatus.{Name:DomainName,Encrypt:EncryptionAtRestOptions.Enabled,N2N:NodeToNodeEncryptionOptions.Enabled,TLS:DomainEndpointOptions.EnforceHTTPS}' --output json
done| Result | Severity | Finding |
|---|---|---|
Encrypt: false on a domain | BREACH RISK | OpenSearch domain {name} unencrypted at rest — must re-create with encryption |
N2N: false | COMPLIANCE GAP | OpenSearch domain {name} lacks node-to-node encryption |
EnforceHTTPS: false | COMPLIANCE GAP | OpenSearch domain {name} accepts plain HTTP |
| All enabled | INFO | OpenSearch encryption complete ✅ |
| No domains | NOT_APPLICABLE | — |
---
FCD7-13: Aurora cluster encryption
aws rds describe-db-clusters --query 'DBClusters[?StorageEncrypted==`false`].{Id:DBClusterIdentifier,Engine:Engine}' --output json| Result | Severity | Finding |
|---|---|---|
| Any unencrypted Aurora cluster | BREACH RISK | Aurora cluster {id} unencrypted — migrate via snapshot-restore |
| All encrypted | INFO | All Aurora clusters encrypted ✅ |
---
Summary
| Check | ID | Key question |
|---|---|---|
| Default EBS encryption | FCD7-01 | Are new volumes encrypted by default? |
| EBS volumes | FCD7-02 | Are existing volumes encrypted? |
| RDS instances | FCD7-03 | Are databases encrypted? |
| RDS snapshots | FCD7-04 | Are backups encrypted? |
| S3 default encryption | FCD7-05 | Are buckets encrypted by default? |
| S3 HTTPS-only | FCD7-06 | Do buckets reject plain HTTP? |
| DynamoDB CMK | FCD7-07 | Do student-data tables use CMK? |
| KMS rotation | FCD7-08 | Are keys automatically rotated? |
| ALB/NLB TLS | FCD7-09 | Do load balancers enforce TLS 1.2+? |
| CloudFront TLS | FCD7-10 | Are distributions HTTPS-only? |
| RDS SSL | FCD7-11 | Do databases require SSL? |
| OpenSearch encryption | FCD7-12 | Is OpenSearch encrypted? |
| Aurora encryption | FCD7-13 | Are Aurora clusters encrypted? |
Total: 13 checks. Expected time: ~3 min.
FCD 8 — Data Minimization, Retention & Secure Destruction — Programmatic Checks
Based on PTAC guidance, NIST SP 800-171 §3.8. Last verified: 2026-05-21.
Execute in order. Read-only AWS CLI. Severity per `../severity-classification.md`.
FCD 8 covers:
- Lifecycle policies on student-data stores (retain-then-delete per schedule)
- Crypto-shred capability for DPA termination (dedicated CMK per district or data category)
- Backup / snapshot retention alignment
- DynamoDB TTL where appropriate
- Secure-destruction evidence
Most FCD 8 findings are COMPLIANCE GAP rather than BREACH RISK — they represent contract-termination and retention-schedule risks rather than active breach vectors.
---
FCD8-01: S3 bucket lifecycle policies
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
lc=$(aws s3api get-bucket-lifecycle-configuration --bucket "$b" --query 'Rules[?Status==`Enabled`]' --output json 2>/dev/null)
[ -z "$lc" ] || [ "$lc" = "null" ] && echo "$b: NO LIFECYCLE"
done| Result | Severity | Finding |
|---|---|---|
| Student-data bucket without lifecycle policy | COMPLIANCE GAP | Bucket {name} has no lifecycle policy — retention is implicit/unbounded |
| Lifecycle present | INFO | Lifecycle configured on {name} ✅ (confirm expiry days match retention schedule) |
Note: "no lifecycle" is not always wrong — some retention schedules require indefinite retention (e.g., transcripts). Surface the buckets without lifecycle and let the user confirm each matches a documented policy.
---
FCD8-02: S3 Object Lock status on long-retention buckets
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
ol=$(aws s3api get-object-lock-configuration --bucket "$b" --query 'ObjectLockConfiguration.ObjectLockEnabled' --output text 2>/dev/null)
echo "$b: Object Lock = $ol"
done| Result | Severity | Finding |
|---|---|---|
| CloudTrail / log-retention bucket without Object Lock | COMPLIANCE GAP | Bucket {name} lacks WORM protection — logs/records could be deleted |
| Student-record buckets requiring WORM without Object Lock | HARDENING GAP | Consider Object Lock (Governance mode) for records under mandatory retention |
| Object Lock enabled where needed | INFO | WORM protection in place ✅ |
Object Lock can only be enabled at bucket creation — flag missing-but-needed as a design finding.
---
FCD8-03: RDS backup retention
aws rds describe-db-instances --query 'DBInstances[].{Id:DBInstanceIdentifier,Retention:BackupRetentionPeriod}' --output jsonMinimum for student-data: ≥7 days (AWS default); many state contracts require ≥35 days.
| Result | Severity | Finding |
|---|---|---|
Any instance with BackupRetentionPeriod: 0 | COMPLIANCE GAP | RDS {id} has backups disabled — enable per recovery-objective + retention policy |
| Any instance with retention < 7 | COMPLIANCE GAP | RDS {id} backup retention is {N} days — below 7-day baseline |
| Retention ≥ baseline | INFO | RDS backup retention adequate ✅ |
---
FCD8-04: AWS Backup plan coverage
aws backup list-backup-plans --query 'BackupPlansList[].{Name:BackupPlanName,Id:BackupPlanId}' --output json
# For each plan:
aws backup get-backup-plan --backup-plan-id {id} --query 'BackupPlan.Rules[].{Name:RuleName,Lifecycle:Lifecycle,Schedule:ScheduleExpression}' --output json| Result | Severity | Finding |
|---|---|---|
| No AWS Backup plans | HARDENING GAP | No centralized backup plan — if RDS PITR is the only backup mechanism, cross-service coverage is limited |
| Backup plan without lifecycle | COMPLIANCE GAP | Backup plan {name} lacks lifecycle — backups never expire or may be deleted prematurely |
| Backup plan with lifecycle aligned | INFO | Backup plan configured with retention ✅ |
---
FCD8-05: DynamoDB TTL for transient student data
aws dynamodb list-tables --query 'TableNames' --output text | tr '\t' '\n' | while read t; do
ttl=$(aws dynamodb describe-time-to-live --table-name "$t" --query 'TimeToLiveDescription.TimeToLiveStatus' --output text 2>/dev/null)
echo "$t: TTL=$ttl"
done| Result | Severity | Finding |
|---|---|---|
| Student-data tables with transient records but no TTL | HARDENING GAP | Table {name} has no TTL — consider for session data, ephemeral records |
| TTL enabled where appropriate | INFO | TTL configured ✅ |
TTL is not universally required — most canonical student records should NOT have TTL. Flag only for transient-record tables identified from Phase 1 scope.
---
FCD8-06: Dedicated CMKs for crypto-shred capability
Crypto-shred pattern: one CMK per district or per data-category, so DPA termination can render the data inert by scheduling key deletion.
aws kms list-keys --query 'Keys[].KeyId' --output text | tr '\t' '\n' | while read k; do
meta=$(aws kms describe-key --key-id "$k" --query 'KeyMetadata.{Manager:KeyManager,Desc:Description,Tags:`tags_placeholder`}' --output json 2>/dev/null)
if echo "$meta" | grep -q '"Manager": "CUSTOMER"'; then
tags=$(aws kms list-resource-tags --key-id "$k" --query 'Tags[?TagKey==`district` || TagKey==`data-category`]' --output json 2>/dev/null)
echo "$k: $meta tags=$tags"
fi
done| Result | Severity | Finding |
|---|---|---|
| Single shared CMK encrypting multiple districts' data | HARDENING GAP | Shared CMK prevents per-district crypto-shred — consider per-district keys for contract-termination data destruction |
| Per-district or per-category CMKs | INFO | Crypto-shred capability per district/category ✅ |
| Single-tenant deployment | NOT_APPLICABLE | — |
This is a design recommendation more than a compliance requirement — surface to the user regardless of severity.
---
FCD8-07: Macie identifies over-collection in S3
Macie findings can reveal fields being collected/stored that aren't needed for the documented purpose — a data-minimization gap.
aws macie2 get-macie-session --query 'status' --output text 2>/dev/null
aws macie2 list-findings --finding-criteria '{"criterion":{"category":{"eq":["SENSITIVE_DATA"]},"archived":{"eq":["false"]}}}' --max-results 50 --query 'findingIds' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| Macie finds unexpected categories of student PII in buckets | COMPLIANCE GAP | Macie detected {category} in bucket {name} — review for over-collection |
| Macie enabled, no surprising findings | INFO | Data-collection scope matches expectations ✅ |
| Macie not enabled | HARDENING GAP | Enable Macie for data-minimization visibility (see also FCD3-08) |
---
FCD8-08: Retention / destruction tags on student-data resources
Heuristic — look for tag keys like retention, retention-expiry, destroy-after, district, student-data.
aws resourcegroupstaggingapi get-resources --resource-type-filters s3 rds dynamodb ec2 --tags-per-page 100 --query 'ResourceTagMappingList[].{Arn:ResourceARN,Tags:Tags[?Key==`retention` || Key==`retention-expiry` || Key==`destroy-after` || Key==`district` || Key==`student-data`]}' --output json| Result | Severity | Finding |
|---|---|---|
| Student-data resources without retention/district tags | COMPLIANCE GAP | {count} student-data resources lack retention or district tags — inventory and lifecycle are ad-hoc |
| All student-data resources tagged | INFO | Tag-based inventory in place ✅ |
---
FCD8-09: Destruction audit trail (CloudTrail key-deletion events)
For the trailing period (check last 90 days), look for KMS key-deletion scheduled events:
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=ScheduleKeyDeletion --max-results 20 --query 'Events[].{Name:EventName,User:Username,Time:EventTime}' --output json 2>/dev/null| Result | Severity | Finding |
|---|---|---|
| Recent ScheduleKeyDeletion events | INFO | Crypto-shred audit trail present — confirm these align with documented DPA-termination events |
| No recent events | INFO | No recent key deletions — no current crypto-shred activity |
Not a compliance finding either way — the check exists to surface the audit trail for reference during a DPA-offboarding review.
---
Summary
| Check | ID | Key question |
|---|---|---|
| S3 lifecycle | FCD8-01 | Are retention policies defined on buckets? |
| S3 Object Lock | FCD8-02 | Are long-retention buckets WORM-protected? |
| RDS backup retention | FCD8-03 | Are backups retained per policy? |
| AWS Backup | FCD8-04 | Is cross-service backup in place? |
| DynamoDB TTL | FCD8-05 | Is transient data auto-expired where appropriate? |
| Per-district CMKs | FCD8-06 | Is crypto-shred possible per district? |
| Macie over-collection | FCD8-07 | Is data collection minimized? |
| Retention tags | FCD8-08 | Are resources tagged for inventory? |
| Destruction audit | FCD8-09 | Is crypto-shred audit trail available? |
Total: 9 checks. Expected time: ~2 min.
Related skills
FAQ
Does ferpa-reviewer certify FERPA compliance?
No. It is an assessment aid, not a certification tool, and its results do not constitute legal advice or guarantee compliance.
Does it cover state student-privacy laws?
Yes. It assesses against FERPA plus common state student-privacy laws and references a state-law addenda file for CA, NY, TX, IL, and others.