<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Janita Madramootoo - OPA</title>
      <link>https://janita.me</link>
      <description>Janita Madramootoo is a software architect in Melbourne, FL, writing about cloud infrastructure, security and compliance, and whatever else is worth taking apart.</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://janita.me/tags/opa/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Mon, 21 Sep 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Passing the CGE-P and Building infra-modules</title>
          <pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
          <author>Janita</author>
          <link>https://janita.me/blog/passing-the-cgep-and-building-infra-modules/</link>
          <guid>https://janita.me/blog/passing-the-cgep-and-building-infra-modules/</guid>
          <description xml:base="https://janita.me/blog/passing-the-cgep-and-building-infra-modules/">&lt;p&gt;I passed the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;grcengclub.com&#x2F;academy&#x2F;cge-p&quot;&gt;GRC Engineer Practitioner&lt;&#x2F;a&gt; (CGE-P) exam and finished the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JanitaM&#x2F;cge-p_capstone&quot;&gt;capstone project&lt;&#x2F;a&gt;!&lt;&#x2F;p&gt;
&lt;p&gt;The most useful thing I built was not part of the course. Partway through, I started &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JanitaM&#x2F;infra-modules&quot;&gt;infra-modules&lt;&#x2F;a&gt; for a client project, and it ended up doing most of the work in my capstone. One capstone pull request put a storage bucket back on default encryption. The pipeline blocked it before anything was built:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;GAP-01 (SOC2-CC6.1): S3 bucket &quot;aws_s3_bucket.uploads&quot; is not encrypted with SSE-KMS using a customer-managed key&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;This post covers how that works.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-i-built-infra-modules&quot;&gt;Why I built infra-modules&lt;&#x2F;h2&gt;
&lt;p&gt;The labs had me write a compliant S3 module (&lt;a href=&quot;https:&#x2F;&#x2F;janita.me&#x2F;blog&#x2F;terraform-compliant-aws-s3-module-nist-800-53&#x2F;&quot;&gt;Lab 2.3&lt;&#x2F;a&gt;), a compliant GCS module (&lt;a href=&quot;https:&#x2F;&#x2F;janita.me&#x2F;blog&#x2F;terraform-compliant-gcp-storage-module&#x2F;&quot;&gt;Lab 2.4&lt;&#x2F;a&gt;), and Rego policies that check a Terraform plan against NIST controls. Then I started building &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;lookatthesebirds.com&#x2F;&quot;&gt;Look At These Birds&lt;&#x2F;a&gt;, a birding blog for a client. The client wanted a secure blog where they owned the code, plus a CMS to add new posts. That ruled out an off-the-shelf CMS, since those providers store the content on their own backend. So I built a lightweight CMS and admin portal on Next.js and AWS.&lt;&#x2F;p&gt;
&lt;p&gt;My site design used S3, CloudFront, WAF, Cognito, SES, DynamoDB, Lambda, and Route 53. Since I was working through the CGE-P labs at the time, I wanted to apply what I was learning in a real project. I made a repo called infra-modules. It started with an S3 module and a policy. It now has 16 AWS modules.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-the-modules-work&quot;&gt;How the modules work&lt;&#x2F;h2&gt;
&lt;p&gt;Two things live in the repo, and they follow different versioning rules.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Modules are pinned. A project references a tag, like &lt;code&gt;?ref=v1.25.0&lt;&#x2F;code&gt;, and upgrades when it chooses to. Each project also commits an &lt;code&gt;infra-modules.yml&lt;&#x2F;code&gt; that declares its version, and CI checks that every module reference matches it.&lt;&#x2F;li&gt;
&lt;li&gt;Policies are not pinned. CI clones the repo at &lt;code&gt;main&lt;&#x2F;code&gt;, runs &lt;code&gt;terraform plan&lt;&#x2F;code&gt;, and checks the plan with Conftest. &lt;code&gt;apply&lt;&#x2F;code&gt; only runs if the check passes.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The unpinned policy clone is on purpose. Every project is held to the current rules, so nothing drifts. The cost is that a new rule can fail an existing project&#x27;s next build. That works for me as a solo developer.&lt;&#x2F;p&gt;
&lt;p&gt;The Rego rules are organized by security intent, like encryption at rest or auditable account activity, not by module. One intent can cover S3 and DynamoDB in a single section. The rules check the plan itself, so they also catch a resource someone wrote by hand outside a module.&lt;&#x2F;p&gt;
&lt;p&gt;I also chose not to write cloud-agnostic modules. A module that abstracts over S3 and GCS hides the provider-specific settings, and those are the settings the policies need to inspect. What I share across clouds is the pattern, not the resource code.&lt;&#x2F;p&gt;
&lt;p&gt;The repo has its own gates. Every Rego rule has an &lt;code&gt;allow&lt;&#x2F;code&gt; fixture and a &lt;code&gt;deny&lt;&#x2F;code&gt; fixture. Module logic is covered by &lt;code&gt;terraform test&lt;&#x2F;code&gt; with a mocked provider. Checkov runs as a blocking check. &lt;code&gt;main&lt;&#x2F;code&gt; is protected, and that applies to me too.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-a-real-project-found&quot;&gt;What a real project found&lt;&#x2F;h2&gt;
&lt;p&gt;As I built the blog, I discovered changes the modules needed. I used Claude Code to work across both repos in parallel and make those changes as I went. Three examples:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The SES bounce topic failed on apply with &lt;code&gt;InvalidSNSDestination: Access denied to KMS key&lt;&#x2F;code&gt;. The module defaulted to the AWS-managed SNS key, and AWS does not let you edit that key&#x27;s policy, so SES could never publish to the topic. The plan looked fine and only a real apply caught it. The module now creates its own customer-managed key with a policy that lets SES publish.&lt;&#x2F;li&gt;
&lt;li&gt;My first CloudTrail rule failed any plan without a trail. That would have broken every other project&#x27;s build, so the rule now checks a trail&#x27;s properties instead.&lt;&#x2F;li&gt;
&lt;li&gt;My DynamoDB and SNS encryption rules fired on resources being destroyed. A deleted resource has no &lt;code&gt;after&lt;&#x2F;code&gt; state in the plan, so every deleted table looked newly non-compliant. I added a helper that skips resources being destroyed and still checks replacements. A rule that fires on false positives gets disabled, and then the gate protects nothing.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;the-capstone&quot;&gt;The capstone&lt;&#x2F;h2&gt;
&lt;p&gt;The capstone project is a patient intake API for a fictional company, &quot;Acme Health&quot;. It shipped with eight known compliance gaps. I chose SOC 2 as the framework instead of HIPAA, the obvious choice for a healthcare app. SOC 2 is the standard software companies get asked about most, and its Type II audits look at whether controls are working over time. A pipeline that signs evidence on every merge produces that kind of proof. SOC 2 does not replace HIPAA, but the two overlap. The starter maps seven of the eight gaps to both a SOC 2 criterion and a HIPAA Security Rule section, so most of the same fixes support both.&lt;&#x2F;p&gt;
&lt;p&gt;I added four layers:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Terraform fixes for six of the eight gaps.&lt;&#x2F;li&gt;
&lt;li&gt;Rego policies that block five of those from coming back.&lt;&#x2F;li&gt;
&lt;li&gt;A GitHub Actions pipeline that plans, checks policy, applies, signs the evidence with Cosign, and uploads it to an Object Lock vault.&lt;&#x2F;li&gt;
&lt;li&gt;An OSCAL component that traces each control to a Terraform resource and to signed evidence.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The brief requires at least five policies, each detecting a real gap. Here is how my eight gaps came out, with the SOC 2 criterion each one maps to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixed, with a policy:&lt;&#x2F;strong&gt; GAP-01 and GAP-02 (encryption with my own KMS key, CC6.1), GAP-03 (deny non-TLS requests, CC6.7), GAP-05 (Lambda in the VPC, CC6.6), and GAP-07 (least-privilege IAM, CC6.3).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Fixed, no policy:&lt;&#x2F;strong&gt; GAP-04, bucket versioning (A1.2). It is one setting, so there is no useful bad change to test against.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Not fixed:&lt;&#x2F;strong&gt; GAP-06 (Lambda concurrency, dead-letter queue, tracing) and GAP-08 (API logging, throttling, firewall). Both map to CC7.2, monitoring. The OSCAL file documents them as planned, not implemented, and a real SOC 2 audit would flag CC7.2 because of them.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The write-up in the repo lists the trade-offs I accepted and what I did not get to.&lt;&#x2F;p&gt;
&lt;p&gt;The evidence vault and the DynamoDB table both come from infra-modules. Both needed to use my own KMS key, and the modules did not support that yet. I extended them and released &lt;code&gt;v1.25.0&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The live apply also caught two mistakes of mine. My notes said GAP-01 and GAP-04 were closed. The KMS key was never wired into the uploads bucket, and the versioning fix sat on a branch that never merged. I fixed both and confirmed with a plan that showed no drift. Now I check the live resources instead of my notes.&lt;&#x2F;p&gt;
&lt;p&gt;The evidence backs up the gate.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JanitaM&#x2F;cge-p_capstone&#x2F;pull&#x2F;12&quot;&gt;PR #12&lt;&#x2F;a&gt; passed and merged, and its run signed the evidence bundle and uploaded it to the vault.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JanitaM&#x2F;cge-p_capstone&#x2F;pull&#x2F;13&quot;&gt;PR #13&lt;&#x2F;a&gt; is the blocked one from the top of this post.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-exam&quot;&gt;The exam&lt;&#x2F;h2&gt;
&lt;p&gt;The exam was 60 multiple-choice questions with 90 minutes to complete it. I finished in about 40 minutes. It costs $350, or it is free for members of the GRC Engineering Club, which I am.&lt;&#x2F;p&gt;
&lt;p&gt;I started the course a couple of months ago and worked on other things in between, so I leaned on the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;grcengclub.com&#x2F;academy&#x2F;CGE-P_Study_Guide.pdf&quot;&gt;study guide&lt;&#x2F;a&gt; to remember the earlier sections. Because I built the labs as I went, and infra-modules on the side, the concepts stuck and the exam was much easier than I expected.&lt;&#x2F;p&gt;
</description>
      </item>
    </channel>
</rss>
