<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Identity Pools | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/identity-pools/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/identity-pools/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Mon, 19 Oct 2026 06:00:12 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>Identity Pools | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/identity-pools/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Building a Secure Patient Document Portal on S3 and Cognito</title>
		<link>https://john-nessime.com/blog/cloud-computing/secure-patient-document-portal-s3-cognito/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Mon, 19 Oct 2026 06:00:12 +0000</pubDate>
				<category><![CDATA[Cloud Computing]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Compliance]]></category>
		<category><![CDATA[Amazon S3]]></category>
		<category><![CDATA[Audit Logging]]></category>
		<category><![CDATA[AWS KMS]]></category>
		<category><![CDATA[Bucket Policy]]></category>
		<category><![CDATA[CloudTrail]]></category>
		<category><![CDATA[Cognito]]></category>
		<category><![CDATA[Encryption]]></category>
		<category><![CDATA[ePHI]]></category>
		<category><![CDATA[Healthcare Cloud]]></category>
		<category><![CDATA[HIPAA]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Pools]]></category>
		<category><![CDATA[Least Privilege]]></category>
		<category><![CDATA[MFA]]></category>
		<category><![CDATA[PHI]]></category>
		<category><![CDATA[Presigned URLs]]></category>
		<category><![CDATA[Session Management]]></category>
		<category><![CDATA[Token Revocation]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=457</guid>

					<description><![CDATA[<p>Cognito for sign-in, S3 for the files, and a demo that passes on the first try. The problems start after that. Prefix isolation, presigned URL lifetimes, KMS as a second gate, CloudTrail data events, and token revocation that actually ends a session.</p>
<p>The post <a href="https://john-nessime.com/blog/cloud-computing/secure-patient-document-portal-s3-cognito/">Building a Secure Patient Document Portal on S3 and Cognito</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The request usually arrives sideways. A clinic manager says something like: &#8220;we&#8217;re emailing scans as PDF attachments and our insurer flagged it, can you build us something?&#8221; Nobody says the words &#8220;patient portal.&#8221; They say &#8220;somewhere for patients to pick up their files.&#8221;</p>



<p class="wp-block-paragraph">So you sketch the obvious thing. Amazon Cognito for sign-in, Amazon S3 for the documents, a small front end in between. It&#8217;s three services and a weekend, and it works on the first try, which is exactly the problem. A <strong>secure patient document portal</strong> built this way passes every test you think to run, because the tests you think to run are all &#8220;can Alice download Alice&#8217;s file?&#8221; The one that matters is &#8220;what can Alice see about Bob?&#8221; and that one has an answer you won&#8217;t like.</p>



<p class="wp-block-paragraph">This post covers the parts that go wrong after the demo. Where to put the authorization boundary, how to scope the S3 policy so one patient can&#8217;t enumerate another, why your download links expire earlier than you configured, what encryption actually gates, and what your audit trail looks like on the day someone asks for it. Everything here is mechanism, not a compliance checklist. Getting a Business Associate Agreement in place is a separate conversation with your lawyer and your AWS account team.</p>



<h2 class="wp-block-heading">The failure that never pages you</h2>



<p class="wp-block-paragraph">Here&#8217;s the shape of it. You write an IAM policy that lets a signed-in user read objects under their own prefix. Something like <code>arn:aws:s3:::clinic-docs/patients/${cognito-identity.amazonaws.com:sub}/*</code>. You test it. Alice gets her file. Alice cannot get Bob&#8217;s file. Ship it.</p>



<p class="wp-block-paragraph">Then, because the front end needs to render a file list, someone adds <code>s3:ListBucket</code>. And <code>s3:ListBucket</code> is not an object-level action. It applies to the bucket ARN, not the object ARN, so the <code>${cognito-identity.amazonaws.com:sub}</code> substitution in your resource string does nothing for it. Unless you add a separate condition on the request, a signed-in patient can list every key in the bucket.</p>



<p class="wp-block-paragraph">They still can&#8217;t download anything. Every <code>GetObject</code> outside their prefix returns a denial. But they can read the key names. And key names in a clinic bucket are rarely neutral. <code>patients/8f2a.../oncology-referral-2024.pdf</code> tells you something about a person even if the bytes stay locked.</p>



<p class="wp-block-paragraph">Nothing alerts on this. No error appears in any log you&#8217;re watching, because nothing errored. The listing succeeded. That&#8217;s the failure mode worth building around: in a document portal, the metadata leak is the one that doesn&#8217;t announce itself.</p>



<p class="wp-block-paragraph">The fix is a condition key on the list action, not a resource string:</p>



<pre class="wp-block-code"><code>{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListOwnPrefixOnly",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::clinic-docs",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "patients/${cognito-identity.amazonaws.com:sub}/*"
          ]
        }
      }
    },
    {
      "Sid": "ReadOwnObjectsOnly",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::clinic-docs/patients/${cognito-identity.amazonaws.com:sub}/*"
    }
  ]
}</code></pre>



<p class="wp-block-paragraph">Two statements, two different jobs. The first constrains what the caller may ask the bucket to enumerate. The second constrains what bytes the caller may pull. Write only the second one and you&#8217;ve secured the download path while leaving the discovery path wide open.</p>



<p class="wp-block-paragraph">One trap in that variable. <code>${cognito-identity.amazonaws.com:sub}</code> resolves to the identity ID from the Cognito <em>identity pool</em>, not the <code>sub</code> claim from the user pool. They are different values for the same human. If you name your prefixes with the user pool sub because that&#8217;s what your application code has handy, the policy will match nothing and every request will be denied, which at least fails loudly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Decide where the authorization boundary lives</h2>



<p class="wp-block-paragraph">There are two honest architectures here and the choice shapes everything downstream. Pick deliberately.</p>



<h3 class="wp-block-heading">Pattern A: browser holds AWS credentials</h3>



<p class="wp-block-paragraph">The user signs in to a Cognito user pool. The application exchanges that token with a Cognito identity pool for temporary AWS credentials, and the browser calls S3 directly using the AWS SDK.</p>



<p class="wp-block-paragraph">It&#8217;s less code and there&#8217;s no server to run. The authorization boundary is entirely the IAM role policy attached to the identity pool. That policy is the whole security model, so it had better be right, and it&#8217;s harder to reason about than application code because the enforcement happens in a service you can&#8217;t step through with a debugger.</p>



<p class="wp-block-paragraph">The real limitation is expressiveness. IAM policy conditions do not know that Bob&#8217;s referral was superseded, that Alice revoked consent last Tuesday, or that a document should only be visible after a clinician releases it. Anything with business logic in it has to be encoded as prefix structure or object tags, and that gets brittle fast.</p>



<h3 class="wp-block-heading">Pattern B: backend brokers every object</h3>



<p class="wp-block-paragraph">The browser never touches AWS credentials. It sends the Cognito access token to your own API. The API validates the token, checks its own records for who may see what, and returns a presigned URL for that one object. S3 is reachable only by the API&#8217;s execution role.</p>



<p class="wp-block-paragraph">You now have a place to put logic, a place to log intent alongside access, and a single choke point to rate limit. The cost is that you own a service, its uptime, and its dependencies.</p>



<p class="wp-block-paragraph">For a small clinic build, this is the one I reach for first. Not because Pattern A is insecure, but because the questions clients ask in month three (&#8220;can we hide results until the doctor reviews them?&#8221;) are answerable in Pattern B and require a re-architecture in Pattern A. The identity-pool approach earns its keep when access really is a pure function of who you are, which in healthcare it rarely stays.</p>



<p class="wp-block-paragraph">If you do want attribute-driven access without a backend, Cognito identity pools can map claims from the token onto principal tags on the temporary session, which you then reference in policy conditions. It works, and it is the right tool for coarse distinctions like patient versus staff. It is not the right tool for per-document consent state.</p>



<h2 class="wp-block-heading">Your download links expire before you think they do</h2>



<p class="wp-block-paragraph">This one generates support tickets on week two. You set a presigned URL to last a few hours. Patients report links dying in under one. Nothing in your code changed.</p>



<p class="wp-block-paragraph">A presigned URL carries a signature made with whatever credentials signed it, and it cannot outlive those credentials. AWS documents the ceilings clearly: with long-lived IAM user keys and Signature Version 4 the maximum is seven days; from the S3 console the range is one minute to twelve hours; with temporary credentials from an assumed role, the URL dies when the role session does, regardless of the expiry you asked for. A default <code>AssumeRole</code> session is one hour.</p>



<p class="wp-block-paragraph">Your Lambda or container signing those URLs is using temporary credentials. So the effective lifetime is the shorter of your requested expiry and the remaining time on that role session, and the remaining time depends on when the runtime last refreshed. That&#8217;s the source of the flapping. You can watch it yourself:</p>



<pre class="wp-block-code"><code># Sign a URL and inspect what was actually encoded into it.
aws s3 presign s3://clinic-docs/patients/example/scan.pdf --expires-in 3600

# The returned URL contains X-Amz-Date, X-Amz-Expires and X-Amz-Credential.
# X-Amz-Expires is the window you asked for. If X-Amz-Credential shows an
# ASIA... key id, you signed with temporary credentials and the real ceiling
# is the session, not the window.</code></pre>



<p class="wp-block-paragraph">The honest resolution is to stop fighting it. Sign short, on demand, when the user clicks. Fifteen minutes is generous for a download the patient is standing in front of. Long links are not a convenience feature in this context anyway, because a presigned URL is a bearer credential: whoever holds the string can fetch the object, signed in or not. Forwarding the email forwards the access. If your portal mails links, you have moved the authorization boundary into someone&#8217;s inbox.</p>



<p class="wp-block-paragraph">Mail a notification that says a document is ready and link to the portal login. Never mail the signed URL itself.</p>



<h2 class="wp-block-heading">Encryption that actually gates something</h2>



<p class="wp-block-paragraph">New S3 objects are encrypted at rest by default with S3-managed keys. That satisfies the sentence &#8220;data is encrypted at rest&#8221; and gates precisely nothing, because anyone with <code>s3:GetObject</code> gets plaintext back. It is a storage property, not an access control.</p>



<p class="wp-block-paragraph">Server-side encryption with an AWS KMS customer managed key is different in kind. Reading an object now requires two independent grants: the S3 permission and <code>kms:Decrypt</code> on the key. Those live in different policies and can be owned by different people. A bucket policy mistake no longer hands over plaintext on its own, and revoking the key grant cuts off reads without touching a single S3 permission.</p>



<p class="wp-block-paragraph">You also get a second audit stream. Every decrypt is a KMS API call, recorded separately from the S3 call, which is useful precisely because an attacker who tampers with one trail probably didn&#8217;t think about the other.</p>



<p class="wp-block-paragraph">The costs are real and worth stating. KMS charges per request, so a portal that fetches many small objects will notice. S3 Bucket Keys reduce that request volume substantially by caching a bucket-level key, at the price of coarser detail in the KMS audit records. For a document portal with modest traffic and high sensitivity, I&#8217;d take the per-request cost and keep the granularity. For a thumbnail-heavy application, the opposite.</p>



<h2 class="wp-block-heading">The audit trail you don&#8217;t have yet</h2>



<p class="wp-block-paragraph">Assume someone will eventually ask: who downloaded this specific file, and when? If you have not explicitly turned something on, the answer is that you don&#8217;t know.</p>



<p class="wp-block-paragraph">CloudTrail is on for management events by default, and management events are control-plane operations: creating the bucket, changing its policy, attaching a role. Object-level reads and writes are <em>data events</em>, and data events are off by default because of volume and cost. <code>GetObject</code> on a patient scan is a data event. Unless you configured a trail with data events for that bucket, no record of the download exists.</p>



<pre class="wp-block-code"><code># Log S3 object-level activity for one bucket on an existing trail.
# Advanced event selectors let you scope to a single bucket prefix rather
# than every object in the account.
aws cloudtrail put-event-selectors 
  --trail-name clinic-audit-trail 
  --advanced-event-selectors '[
    {
      "Name": "PHI bucket object access",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Data"]},
        {"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
        {"Field": "resources.ARN", "StartsWith":
          ["arn:aws:s3:::clinic-docs/patients/"]}
      ]
    }
  ]'</code></pre>



<p class="wp-block-paragraph">S3 server access logs are the other option and they are not equivalent. They record HTTP-level request detail and are delivered on a best-effort basis with no delivery guarantee, which makes them useful for traffic analysis and weak as evidence. CloudTrail data events carry caller identity, which is the field you actually need when the question is &#8220;who.&#8221;</p>



<p class="wp-block-paragraph">Two things to sort out while you&#8217;re in there. Send the trail to a bucket in a different account, or at minimum a bucket the portal&#8217;s roles cannot write to, so that compromising the application doesn&#8217;t compromise its own log. And put a lifecycle rule on the log bucket, because data event volume grows with usage and nobody budgets for it. If you already run centralized dashboards on something like Grafana Cloud or CloudWatch, wire an alert to unusual read volume per identity now rather than after the incident.</p>



<h2 class="wp-block-heading">Cutting off access when someone leaves</h2>



<p class="wp-block-paragraph">Disabling a Cognito user does not immediately end their session, and this surprises people. Tokens are self-contained. An access token already in a browser stays cryptographically valid until it expires, typically about an hour.</p>



<p class="wp-block-paragraph">Refresh tokens are the longer exposure. The default is thirty days, and app clients can be configured anywhere from sixty minutes to ten years. Ten-year refresh tokens on a portal holding medical records is not a hypothetical misconfiguration; it&#8217;s a checkbox someone ticked to stop users complaining about re-login.</p>



<p class="wp-block-paragraph">Cognito gives you revocation. Token revocation is enabled by default on newly created app clients. <code>AdminUserGlobalSignOut</code> revokes all of a user&#8217;s refresh, ID, and access tokens from the server side, and the <code>/oauth2/revoke</code> endpoint kills one refresh token and everything descended from it.</p>



<pre class="wp-block-code"><code># Server-side, authorized with IAM credentials. Revokes the user's refresh,
# ID and access tokens.
aws cognito-idp admin-user-global-sign-out 
  --user-pool-id us-east-1_EXAMPLE 
  --username patient-example

# Confirm revocation is switched on for the app client before you rely on it.
aws cognito-idp describe-user-pool-client 
  --user-pool-id us-east-1_EXAMPLE 
  --client-id EXAMPLECLIENTID 
  --query 'UserPoolClient.EnableTokenRevocation'</code></pre>



<p class="wp-block-paragraph">Now the part that catches teams out. AWS documents that a revoked token still passes verification in a generic JWT library that only checks signature and expiry. Revocation is enforced by Cognito, not baked into the token. If your API validates tokens by fetching the JWKS and calling a verify function, a revoked token sails straight through until it expires on its own.</p>



<p class="wp-block-paragraph">Which means short access token lifetimes are load-bearing. Revocation shortens the tail; it doesn&#8217;t close the window. Refresh token rotation, available on the Essentials and Plus feature tiers, replaces the refresh token on each exchange and narrows the window a stolen one is useful for. If you&#8217;re paying for Plus for threat protection features, you already have it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Troubleshooting the usual suspects</h2>



<h3 class="wp-block-heading">Every request returns access denied after login</h3>



<p class="wp-block-paragraph">Nine times out of ten the prefix in your policy variable doesn&#8217;t match the prefix in your bucket. Print the identity ID your session actually resolved to and compare it character by character against a real key. Remember the identity pool ID and the user pool sub are different values.</p>



<h3 class="wp-block-heading">Signature does not match on a presigned URL</h3>



<p class="wp-block-paragraph">Usually something modified the request after signing. A client library adding a header, a proxy rewriting the path, or an email gateway URL-encoding the query string. Compare the <code>X-Amz-SignedHeaders</code> list in the URL against what the client actually sent.</p>



<h3 class="wp-block-heading">Uploads succeed but downloads fail</h3>



<p class="wp-block-paragraph">Check the KMS key policy separately from the IAM policy. A role can hold <code>kms:GenerateDataKey</code> (enough to write) without <code>kms:Decrypt</code> (needed to read). The S3 error will not say the word KMS prominently, which is why this one burns an afternoon.</p>



<h3 class="wp-block-heading">Browser uploads fail before any request reaches S3</h3>



<p class="wp-block-paragraph">That&#8217;s CORS. The bucket needs a CORS configuration allowing your portal&#8217;s origin, the methods you use, and any headers the SDK sends. Browser preflight failures never appear in S3 logs, so check the browser console first.</p>



<h3 class="wp-block-heading">Nothing appears in CloudTrail</h3>



<p class="wp-block-paragraph">Data events are not shown in CloudTrail Event History. Query the destination bucket directly, with Athena or CloudTrail Lake, and allow a few minutes for delivery.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li>Granting <code>s3:ListBucket</code> without an <code>s3:prefix</code> condition, so patients can enumerate every key even though downloads are blocked.</li>

<li>Putting identifying information in object keys or file names. Assume the key will leak eventually and use opaque identifiers.</li>

<li>Emailing presigned URLs. The link is the credential, and inboxes get forwarded, backed up, and breached.</li>

<li>Extending refresh token lifetime to silence complaints about re-login, instead of implementing rotation.</li>

<li>Assuming default encryption means access control. It means the disk is encrypted, nothing more.</li>

<li>Enabling versioning without a lifecycle rule, so deleted documents linger indefinitely as noncurrent versions you forgot exist.</li>

<li>Skipping CloudTrail data events to save money, then discovering the gap during an access review.</li>
</ul>



<h2 class="wp-block-heading">Best practices for a secure patient document portal</h2>



<ul class="wp-block-list">
<li>Keep one bucket for patient documents and nothing else. Mixed-purpose buckets make policies ambiguous and audits slow.</li>

<li>Turn on S3 Block Public Access at the account level, not just the bucket. Account level survives someone creating a new bucket in a hurry.</li>

<li>Enable versioning and set a noncurrent version expiration rule, so an overwritten document is recoverable but not permanent.</li>

<li>Use a KMS customer managed key so decrypt authority is separable from S3 authority.</li>

<li>Require MFA for staff accounts. TOTP costs nothing to enable and staff accounts are the ones with breadth.</li>

<li>Sign download URLs on demand with short expiry, and always from a backend that logged why it signed.</li>

<li>Put the portal behind a WAF and rate limiting. Cloudflare in front of the application, or AWS WAF on the distribution, both work; the point is that credential stuffing against a login page is the most likely thing to actually happen to you.</li>

<li>Restrict administrative access to the AWS console and to any bastion by network as well as identity, whether that&#8217;s IAM Identity Center with condition keys or a VPN such as NordVPN or Surfshark for a small team without central identity.</li>

<li>Write down the retention period before launch, then encode it as a lifecycle rule. Retention decided later is retention never decided.</li>
</ul>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Are S3 and Cognito enough for HIPAA compliance?</h3>



<p class="wp-block-paragraph">Both are covered under AWS&#8217;s HIPAA eligibility program, and Cognito&#8217;s documentation states it is HIPAA-BAA eligible. That makes them usable building blocks, not compliance. You need a signed Business Associate Agreement with AWS, and the configuration, access reviews, training, and documented policies remain yours. Service eligibility is a prerequisite, not an outcome.</p>



<h3 class="wp-block-heading">Should I use a Cognito identity pool or a backend API?</h3>



<p class="wp-block-paragraph">Use an identity pool when access is a pure function of identity and the rules will not change. Use a backend when documents have states, when a clinician gates release, or when you need to log intent alongside access. Most clinic portals become the second within a year.</p>



<h3 class="wp-block-heading">How long should a presigned URL last?</h3>



<p class="wp-block-paragraph">Minutes, not hours. Sign at click time. The maximum with long-lived credentials is seven days, but that ceiling is irrelevant here because the link is a bearer credential and a longer window is a longer leak.</p>



<h3 class="wp-block-heading">Can patients upload documents back to the portal?</h3>



<p class="wp-block-paragraph">Yes, using presigned PUT or presigned POST. Presigned POST is worth knowing about because its policy document can constrain content type and size before the object lands, which presigned PUT cannot. Either way, write uploads into a quarantine prefix, scan them, then move them.</p>



<h3 class="wp-block-heading">Does enabling CloudTrail data events cost much?</h3>



<p class="wp-block-paragraph">Data events are billed per event recorded with no free tier, unlike management events. For a portal serving a few thousand downloads a month the amount is small; for a bucket also serving application assets it is not. Scope your event selector to the patient prefix rather than the whole account and the question mostly goes away.</p>



<h3 class="wp-block-heading">What happens to sessions when I disable a user?</h3>



<p class="wp-block-paragraph">Nothing immediate. Existing access tokens remain valid until they expire. Call <code>AdminUserGlobalSignOut</code> to revoke tokens, and keep access token lifetime short so the residual window is measured in minutes.</p>



<h3 class="wp-block-heading">Do I need CloudFront in front of the bucket?</h3>



<p class="wp-block-paragraph">Not for correctness. It helps when patients are geographically spread, when you want signed cookies for a set of objects rather than one URL per object, or when you want a single edge to attach a WAF to. For a single-clinic portal, direct presigned URLs are simpler and one fewer thing to misconfigure.</p>



<h2 class="wp-block-heading">The one thing to remember</h2>



<p class="wp-block-paragraph">A signed-in token proves who someone is. It does not decide what they may reach. In a <strong>secure patient document portal</strong> the boundary is the policy condition, the KMS grant, and the code that decides whether to sign a URL at all, and every one of those is something you have to write on purpose.</p>



<p class="wp-block-paragraph">The demo passes either way. The difference only shows up when someone reads a key name they shouldn&#8217;t have seen, and nothing in your system considers that an error.</p>



<p class="wp-block-paragraph">If you build one of these, spend an hour before launch on exactly two things: sign in as a test patient and try to list the bucket, then ask your logs who downloaded a specific file last week. If either answer is uncomfortable, you&#8217;ve found your next task.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second pair of eyes on your portal?</h2>



<p class="wp-block-paragraph">This is the kind of build where the gap between working and safe is invisible from the outside. I help small teams close it.</p>



<ul class="wp-block-list">
<li>Reviewing S3 bucket policies and Cognito identity pool roles for prefix isolation gaps, including the list-versus-get split most policies miss.</li>

<li>Designing the boundary: identity pool credentials in the browser versus a backend that brokers presigned URLs, with the trade-offs written down for your stakeholders.</li>

<li>Setting up KMS customer managed keys, key policies, and grant separation so decrypt authority is not implied by storage authority.</li>

<li>Turning on scoped CloudTrail data events, building the Athena tables to query them, and alerting on abnormal per-identity read volume.</li>

<li>Hardening the Cognito side: MFA, token lifetimes, refresh token rotation, and a revocation path that actually ends sessions.</li>

<li>Writing the whole thing as Terraform or OpenTofu so the next change is a reviewed pull request rather than a console click.</li>
</ul>



<p class="wp-block-paragraph">Send me your bucket policy and your identity pool role, with the account IDs redacted, and I&#8217;ll tell you what a signed-in patient can actually see.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/cloud-computing/secure-patient-document-portal-s3-cognito/">Building a Secure Patient Document Portal on S3 and Cognito</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
