Please note: this is a self-attested proof of concept. The app is not FedRAMP Certified and never will be. There is no agency, no Authorization to Operate, no government data.
The app, and why it's a fair test case
Along with this website I run a small web app that coaches my Tai Chi practice. I built it for fun, but somewhere along the way it turned into a real SaaS: it logs people in, it stores secrets, it makes outbound calls to an AI service, and it has an API sitting behind a gateway. That is the same architecture as most small SaaS products on the market, and it is also the exact shape that, in federal-land, normally takes time and money to wrap in compliance. I wanted to see how far one person could get in a few days. (Aside: if you do Tai Chi and want to help me beta test the app, reach out!)
Step 1: Hardening my app
I knew I needed to uplevel my security if I wanted to be FedRAMP audit-ready. First I dealt with how people get in. The app used to hide behind a single shared password, which is not really authentication. I replaced it with Amazon Cognito and made multi-factor authentication mandatory - required, not optional - with a 14-character password minimum and authenticator-app codes instead of SMS, which is both more secure and free. The whole thing is open source, so the exact Terraform is there to read if you want the details.
From there I gated the data behind that login. It is easy to put a login screen in front of an app and still leave the API wide open underneath, so I gave the API gateway a JWT authorizer tied to the Cognito pool and attached it only to the data routes. The gateway now turns away any request to /api/... without a valid token before my Lambda code runs, while the login page stays open so it can load for anyone - 200 on the page, 401 on the data API, as you will see below.
I then turned on DNSSEC so the domain validates all the way to the root, which took the only manual step in the whole project: Terraform emits the DS record and I pasted it into my registrar by hand, because that part lives outside the AWS account. And I had the daily drift check sign its own output with a KMS key, publishing the public key at /.well-known/runtime-signing-pubkey.pem so anyone can verify the freshness without taking my word for it. (An aside on the jargon: FedRAMP 20x leans on KSIs, Key Security Indicators - small machine-checkable assertions like "the bucket blocks public access" instead of paragraphs of prose.)
Step 2: Running a self-assessment
I worked through the controls myself and self-attest that they are met; the generated security plan and decision record further down are that attestation, in machine-readable form. On top of that I wanted an automated second opinion, so I leaned on someone else's excellent work. Ethan Troy built a set of FedRAMP 20x KSI validation frameworks for Prowler, the open-source cloud scanner, that turn the indicators into a single command. One thing to be plain about: the baseline I ran covers the 20x KSIs at the Low tier (the A and B evidence levels), so the scan is a floor under the self-attestation, not a stand-in for it. I pointed it at my account with prowler aws --compliance fedramp_20x_ksi_low_aws, and the first run came back with 33 failures. Not a great look, but that is the point of running it.
Working the findings, one at a time
I worked them in the open. You can see the steps I took in each GitHub PR.
The first two were leftover administrator identities from when I first set the account up. One was an old CLI user called CLI_admin with AdministratorAccess attached that I had not touched in over a year; the other was a steampipe-user I had created to try a tool, with a stale access key and no MFA. Neither needed to exist, so I deleted both.
Next, the account password policy was weak, so I tightened it to a real one - length 14, a 90-day max age, no reuse of the last 24 - which cleared the whole family of iam_password_policy_* findings in one move.
Then a subtler one. My own permissions were stapled directly onto my IAM user instead of going through a group, nine policies in all. That is exactly the sloppiness the finding exists to catch, because direct-attached permissions are hard to audit and easy to forget about. I created an operators group, moved all nine onto it, added my user to the group, and detached the direct ones. Same access, now in a place I can actually reason about.
(An aside on the jargon: a POA&M, Plan of Action and Milestones, is just the official list of known-open problems and what you intend to do about each.)
Some findings had no quick or cheap fix, and I wrote those into the POA&M instead of pretending they were gone. For example, the root account and my operator user both use a virtual authenticator-app code, not a hardware key. The control that wants multi-factor is met, because there is MFA on both. What is not met is the stricter phishing-resistant objective, which wants a hardware token or a passkey. I do not have a hardware token on hand yet (they are not cheap), so I logged it as POAM-025, an open item with the compensating control (virtual MFA is on now) and the planned fix (enroll a hardware key) written down honestly.
Then I ran the same command again. It dropped from 33 failures to 11 - twenty-four percent down to nine. Every one of those eleven is accounted for, either written down as accepted risk or sitting in the POA&M behind a named fix like POAM-025, with nothing swept under the rug.
Audit-ready ... But don't take my word for it!
None of this is worth anything if it isn't verifiable, so here is how you can check my work, easily and quickly. (Aside: OSCAL is NIST's machine-readable format for security documents, so the security plan below is real OSCAL JSON, not a PDF.)
First, that the login actually guards the app. The page loads for anyone; the data behind it does not:
curl -s -o /dev/null -w "%{http_code}\n" https://samaydlette.com/silk-reeling/
# 200 - the app loads for anyone
curl -s -o /dev/null -w "%{http_code}\n" https://samaydlette.com/silk-reeling/api/exercises
# 401 - the data API will not talk without a tokenNext, the compliance artifacts, all served under /.well-known/. The traditional FedRAMP track (Rev5) is built around a System Security Plan; the new 20x track does away with that document and replaces it with a Security Decision Record. I generate both, from the same inventory, because I am keeping the system valid under both tracks at once - so you can check either one. Start with the Rev5 plan and read back its own title:
curl -s https://samaydlette.com/.well-known/oscal-ssp.json | jq -r '.["system-security-plan"].metadata.title'
# samaydlette.com — System Security Plan (Self-Attested PoC, NOT FedRAMP-Certified)Then the 20x equivalent. The Decision Record carries the same posture as a set of per-indicator records rather than one big per-control plan:
curl -s https://samaydlette.com/.well-known/security-decision-record.json | jq -r '"\(.record_type) - \(.class), \(.rule_records | length) indicators"'
# security-decision-record - Class C, 46 indicatorsA few more one-liners give you the rest - the component count, the vulnerability summary, the daily runtime signal's timestamp (when I ran them just now: 211 components, 18 findings and zero blocking, and a timestamp from this morning). And it is all Sigstore-signed: cosign verify-blob against the exact GitHub Actions workflow identity proves who published the bytes, though not that the claims inside are true - the schema checks and the deploy gate handle that part. Or skip the terminal and just look at the dashboard, which renders all of the same files.
The pipeline IS the documentation
The thing that makes the documentation trustworthy is that I don't touch it. Every deploy, a set of generator scripts reads the system as it actually is and emits the documents from it. build-ksi-signal.py walks the Terraform state and inventories every live resource into the canonical signal; build-oscal-ssp.py turns that into the system security plan; build-oscal-poam.py emits the POA&M; the VDR builder emits the vulnerability report. They all derive from the one inventory, so they cannot quietly disagree. For the full walk-through of how that is wired, I wrote it up separately in Shift Left on Reciprocity.
And if they do disagree, the deploy fails. An OPA policy is the gate - OPA is a policy engine, and you write the rules in a language called Rego - so any check that comes back non-compliant stops the deploy before anything ships. For a deeper dive on how I use OPA, see my earlier write-up on it. A second gate runs after it - a reconciliation step that fails closed if the live account and the artifacts have drifted apart - and only once both pass do the artifacts get signed with cosign and uploaded. The signing is keyless and logged in Sigstore's public transparency log, which is why the verify command above can pin the exact workflow that produced the file.
There was a real test of this while I was writing. FedRAMP published the final version of its 2026 rulebook on the morning of June 24, and a rulebook change is normally what sends a compliance team back into a pile of documents for weeks. Because my documents are generated, realigning to the final rules was a code change, not a document-rewrite project. My first pass was the same morning - I updated the renamed rule identifiers, the relabeled vulnerability family, and the new effective dates in the generator scripts, reran the pipeline, and the artifacts started coming out aligned to the published rules. The deeper changes - a renumbered set of security indicators and a couple of new document types the rules now ask for - followed the next day, each one a generator edit and a redeploy rather than a new binder. All of it is live now, generated from the same inventory as everything else.
Lessons learned
In my opinion the surprising thing about this project was how little of it was security. Cognito, MFA, DNSSEC, key management, logging, etc. was all just a few days of work, and most of it was a few lines of Terraform each. The scanner findings were an afternoon. None of it was hard.
In my opinion the only thing that is normally hard about this kind of effort, and the thing that was completely absent here, is coordination. In a normal organization the person who finds the issue, the person who fixes it, the person who signs off on the risk, and the person who owns the system are four different people on four different teams. Here they were all me. I am not claiming that scales, and I am not drawing a grand conclusion from it. It is just the part I noticed. When one person holds every role, the handoffs that usually eat the schedule simply are not there. This backs up my earlier observation about the rise of the Transformation Engineer role.