<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ secrets management - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ secrets management - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 26 Jul 2026 22:33:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/secrets-management/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Sync AWS Secrets Manager Secrets into Kubernetes with the External Secrets Operator ]]>
                </title>
                <description>
                    <![CDATA[ If someone asked you how secrets flow from AWS Secrets Manager into a running pod, could you explain it confidently? Storing them is straightforward. But handling rotation, stale env vars, and the gap ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-sync-aws-secrets-manager-secrets-into-kubernetes-with-the-external-secrets-operator/</link>
                <guid isPermaLink="false">69c541f010e664c5dadc877e</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cybersecurity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Terraform ]]>
                    </category>
                
                    <category>
                        <![CDATA[ secrets management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SRE ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Osomudeya Zudonu ]]>
                </dc:creator>
                <pubDate>Thu, 26 Mar 2026 14:25:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6cca126e-dd50-4400-ae9d-65449581345b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If someone asked you how secrets flow from AWS Secrets Manager into a running pod, could you explain it confidently?</p>
<p>Storing them is straightforward. But handling rotation, stale env vars, and the gap between what your pod reads and what AWS actually holds is where many engineers go quiet.</p>
<p>In this guide, you'll build a complete secrets pipeline from AWS Secrets Manager into Kubernetes pods. You'll provision the infrastructure with Terraform, sync secrets using the External Secrets Operator, and run a sample application that reads the same credentials in two different ways: via environment variables and via a volume mount.</p>
<p>By the end, you'll be able to:</p>
<ul>
<li><p>Explain the full architecture from vault to pod</p>
</li>
<li><p>Run the lab locally in about 15 minutes</p>
</li>
<li><p>Prove why environment variables go stale after rotation, while mounted secret files stay fresh</p>
</li>
<li><p>Deploy the same pattern on Amazon Elastic Kubernetes Service with OpenID Connect-based CI/CD</p>
</li>
<li><p>Troubleshoot the most common failures</p>
</li>
</ul>
<p>Below is an architecture diagram showing secrets flowing from AWS Secrets Manager through the External Secrets Operator into a Kubernetes Secret, then splitting into environment variables set at pod start and a volume mount that updates within 60 seconds.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/ac8bfc9e-304e-41b8-b6a3-7ce1795b29a9.png" alt="Architecture diagram showing secrets flowing from AWS Secrets Manager through the External Secrets Operator into a Kubernetes Secret, then splitting into environment variables set at pod start and a volume mount that updates within 60 seconds." style="display:block;margin:0 auto" width="1024" height="1536" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-to-understand-the-secret-flow">How to Understand the Secret Flow</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-local-lab">How to Run the Local Lab</a></p>
</li>
<li><p><a href="#heading-how-to-inspect-the-externalsecret-and-the-application">How to Inspect the ExternalSecret and the Application</a></p>
</li>
<li><p><a href="#heading-how-to-test-secret-rotation">How to Test Secret Rotation</a></p>
</li>
<li><p><a href="#heading-how-to-choose-between-external-secrets-operator-and-the-csi-driver">How to Choose Between External Secrets Operator and the CSI Driver</a></p>
</li>
<li><p><a href="#heading-how-to-deploy-the-pattern-on-amazon-elastic-kubernetes-service">How to Deploy the Pattern on Amazon Elastic Kubernetes Service</a></p>
</li>
<li><p><a href="#heading-how-to-configure-github-actions-without-stored-aws-credentials">How to Configure GitHub Actions Without Stored AWS Credentials</a></p>
</li>
<li><p><a href="#heading-how-to-troubleshoot-the-most-common-failures">How to Troubleshoot the Most Common Failures</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have the following tools installed and configured.</p>
<p><strong>For the local lab:</strong></p>
<ul>
<li><p>An AWS account with access to AWS Secrets Manager</p>
</li>
<li><p>The AWS CLI installed and configured. Run <code>aws configure</code> and provide your access key, secret key, default region, and output format. The credentials need permission to read and write secrets in AWS Secrets Manager.</p>
</li>
<li><p><code>kubectl</code> installed. For Microk8s, run <code>microk8s kubectl config view --raw &gt; ~/.kube/config</code> after installation to connect kubectl to your local cluster.</p>
</li>
<li><p>Terraform installed</p>
</li>
<li><p>Helm installed</p>
</li>
<li><p>Docker installed</p>
</li>
<li><p>A local Kubernetes cluster: the lab supports Microk8s and kind. If you do not have either installed, follow the <a href="https://microk8s.io/">Microk8s install guide</a> before continuing.</p>
</li>
</ul>
<p><strong>For the Amazon Elastic Kubernetes Service sections:</strong></p>
<ul>
<li><p>An Amazon Elastic Kubernetes Service cluster you can create or manage</p>
</li>
<li><p>A GitHub repository you can configure for workflows and secrets</p>
</li>
</ul>
<p>The lab repository includes two deployment paths: a local path for fast learning and an Amazon Elastic Kubernetes Service path for a production-like setup. All the exact commands for each path live in the repo's <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/docs/DEPLOY-LOCAL.md"><code>docs/DEPLOY-LOCAL.md</code></a> and <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/docs/DEPLOY-EKS.md"><code>docs/DEPLOY-EKS.md</code></a>.</p>
<h2 id="heading-how-to-understand-the-secret-flow">How to Understand the Secret Flow</h2>
<p>Before you run any command, you need to understand how the pieces connect.</p>
<p>The flow has four stages:</p>
<ol>
<li><p>A developer or automated system updates a secret in AWS Secrets Manager.</p>
</li>
<li><p>The External Secrets Operator polls AWS Secrets Manager on a schedule and creates or updates a Kubernetes Secret.</p>
</li>
<li><p>Your pod reads that Kubernetes Secret.</p>
</li>
<li><p>During rotation, the Kubernetes Secret updates, but your two consumption modes behave differently.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/9dc52f99-add4-490a-ad86-25a30d0ae306.png" alt="A step-by-step flow diagram showing the four stages of secret flow above" style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h3 id="heading-how-the-external-secrets-operator-sync-works">How the External Secrets Operator Sync Works</h3>
<p>The External Secrets Operator reads a custom Kubernetes resource called <code>ExternalSecret</code>. That resource tells the operator three things:</p>
<ul>
<li><p>Which secret store to connect to</p>
</li>
<li><p>Which Kubernetes Secret name to create or update</p>
</li>
<li><p>How often to refresh</p>
</li>
</ul>
<p>In this lab, the <code>ExternalSecret</code> creates a Kubernetes Secret named <code>myapp-database-creds</code>. The operator also adds a template annotation that can trigger a pod restart when the secret rotates.</p>
<h3 id="heading-how-the-app-consumes-secrets">How the App Consumes Secrets</h3>
<p>The sample application exposes three endpoints so you can validate behavior at any time.</p>
<ul>
<li><p><code>/secrets/env</code> shows what environment variables the pod sees</p>
</li>
<li><p><code>/secrets/volume</code> shows what files in the mounted secret directory look like</p>
</li>
<li><p><code>/secrets/compare</code> compares both and reports whether rotation has been detected</p>
</li>
</ul>
<p>The app checks four keys: <code>DB_USERNAME</code>, <code>DB_PASSWORD</code>, <code>DB_HOST</code>, and <code>DB_PORT</code>.</p>
<h2 id="heading-how-to-run-the-local-lab">How to Run the Local Lab</h2>
<p>The local lab gives you a fast learning loop. You can see the full pipeline working and test rotation without waiting for a cloud deployment.</p>
<h3 id="heading-step-1-clone-the-repo">Step 1: Clone the Repo</h3>
<pre><code class="language-bash">git clone https://github.com/Osomudeya/k8s-secret-lab
cd k8s-secret-lab
</code></pre>
<h3 id="heading-step-2-run-the-spin-up-script">Step 2: Run the Spin-Up Script</h3>
<pre><code class="language-bash">bash spinup.sh
</code></pre>
<p>The script will ask you to choose a local cluster type. Pick Microk8s or kind, depending on what you have installed. The script installs the External Secrets Operator via Helm, applies the Terraform configuration, and deploys the sample application.</p>
<p>If the script fails at any point, check <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/docs/TROUBLESHOOTING.md"><code>docs/TROUBLESHOOTING.md</code></a> before retrying. The most common causes are missing AWS credentials, a misconfigured kubeconfig, or a Microk8s storage add-on that is not enabled.</p>
<h3 id="heading-important-run-the-lab-ui">Important: Run the Lab UI</h3>
<p>The lab ships with a separate guided tutorial interface that runs on your laptop. This is not the in-cluster application, it's a React-based checklist at <code>lab-ui/</code> that walks you through each concept and checkpoint as you work through the lab.</p>
<p>To start it, open a second terminal and run:</p>
<pre><code class="language-bash">cd lab-ui &amp;&amp; npm install &amp;&amp; npm run dev
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/873166e9-6bff-4e56-a18d-e58b9e9a5af9.png" alt="Screenshot of npm run dev lab ui terminal" style="display:block;margin:0 auto" width="849" height="435" loading="lazy">

<p>Then open <a href="http://localhost:5173"><code>http://localhost:5173</code></a>. You'll see a module-by-module guide covering the full flow from external secrets to rotation to CI/CD.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/5a5b220b-3f23-4c7c-8388-f2e23d122e2c.png" alt="Screenshot of The Lab UI, a guided tutorial interface that runs alongside the lab and walks you through each concept and checkpoint." style="display:block;margin:0 auto" width="1399" height="1287" loading="lazy">

<p>Keep this terminal running alongside your lab. The Lab UI and the in-cluster app (<code>localhost:3000</code>) are two separate things, the UI guides you through the steps, the app shows you the live secrets.</p>
<h3 id="heading-step-3-access-the-application">Step 3: Access the Application</h3>
<p>Once the lab finishes, port-forward the service.</p>
<pre><code class="language-bash">kubectl port-forward svc/myapp 3000:80 -n default
</code></pre>
<p>Open <code>http://localhost:3000</code>. You should see a table showing each secret key and whether the environment variable value matches the volume mount value.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/dbe122ac-b787-40d0-96f4-4b1276bab017.png" alt="Screenshot of the running application at localhost:3000. Every row in the table should show &quot;Match ✓" style="display:block;margin:0 auto" width="1213" height="902" loading="lazy">

<h3 id="heading-step-4-validate-that-secrets-match">Step 4: Validate That Secrets Match</h3>
<p>Run the compare endpoint directly from the terminal.</p>
<pre><code class="language-bash">curl -s http://localhost:3000/secrets/compare | python3 -m json.tool
</code></pre>
<p>When everything is working, the response will include <code>"all_match": true</code>.</p>
<h2 id="heading-how-to-inspect-the-externalsecret-and-the-application">How to Inspect the ExternalSecret and the Application</h2>
<p>At this point the lab is running. Now you'll want to inspect the manifests so you understand what each part does.</p>
<h3 id="heading-step-1-read-the-externalsecret-manifest">Step 1: Read the ExternalSecret Manifest</h3>
<p>Open <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/k8s/aws/external-secret.yaml"><code>k8s/aws/external-secret.yaml</code></a>. Focus on these four fields:</p>
<ul>
<li><p><code>refreshInterval</code>: how often the operator polls AWS Secrets Manager</p>
</li>
<li><p><code>secretStoreRef</code>: which store the operator authenticates against</p>
</li>
<li><p><code>target</code>: the name of the Kubernetes Secret to create</p>
</li>
<li><p><code>data</code>: the mapping from AWS Secrets Manager JSON keys to Kubernetes Secret keys</p>
</li>
</ul>
<p>Here is what that mapping looks like in this lab:</p>
<pre><code class="language-yaml">spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: myapp-database-creds
    creationPolicy: Owner
  data:
    - secretKey: DB_USERNAME
      remoteRef:
        key: prod/myapp/database
        property: username
</code></pre>
<p>The <code>property</code> field tells the operator which JSON key inside the AWS secret to extract. If your secret in AWS Secrets Manager is a JSON object, each field gets its own entry here.</p>
<p>Two fields here are worth understanding before you move on. <code>creationPolicy: Owner</code> means the operator owns the Kubernetes Secret it creates. If you delete the <code>ExternalSecret</code>, the Secret is deleted too. <code>ClusterSecretStore</code> is a cluster-scoped store, meaning any namespace in the cluster can use it. A plain <code>SecretStore</code> is namespace-scoped. For this lab, cluster-scoped is the right choice because it keeps the setup simple.</p>
<h3 id="heading-step-2-read-the-deployment-manifest">Step 2: Read the Deployment Manifest</h3>
<p>Open <a href="http://github.com/Osomudeya/k8s-secret-lab/blob/main/k8s/aws/deployment.yaml"><code>k8s/aws/deployment.yaml</code></a>. You are looking for two sections: <code>envFrom</code> and <code>volumeMounts</code>.</p>
<pre><code class="language-yaml">envFrom:
  - secretRef:
      name: myapp-database-creds

volumeMounts:
  - name: db-secret-vol
    mountPath: /etc/secrets
    readOnly: true
</code></pre>
<p>Both paths read from the same Kubernetes Secret, <code>myapp-database-creds</code>. The <code>envFrom</code> block injects all keys as environment variables at pod start.<br>The <code>volumeMounts</code> block mounts the same secret as files under <code>/etc/secrets</code>.</p>
<p>This is the core of the rotation lesson. Both paths read the same source. But they behave differently after that source changes.</p>
<h3 id="heading-step-3-read-the-app-comparison-logic">Step 3: Read the App Comparison Logic</h3>
<p>Open <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/app/server.js"><code>app/server.js</code></a>. The comparison logic reads environment variables from <code>process.env</code> and reads mounted secret files from <code>/etc/secrets/&lt;key&gt;</code>. Then it computes a per-key match and a global <code>all_match</code> value.</p>
<p>The <code>/secrets/compare</code> endpoint sets <code>rotation_detected: true</code> when any key differs between env and volume.</p>
<h2 id="heading-how-to-test-secret-rotation">How to Test Secret Rotation</h2>
<p>Secret rotation is where real teams feel pain. This lab makes that pain visible so you can explain it clearly and fix it confidently.</p>
<h3 id="heading-how-the-rotation-gap-works"><strong>How the Rotation Gap Works</strong></h3>
<p>When a pod starts, Kubernetes gives it two ways to read a secret.</p>
<p>The first way is environment variables. Think of these like sticky notes written on the wall of the container the moment it boots up. The value gets written once, at startup, and never changes. Even if the secret in AWS gets updated ten minutes later, the sticky note still says the old value. The container cannot see the update because nobody rewrote the note.</p>
<p>The second way is a volume mount. Think of this like a shared folder that someone else can update remotely. Kubernetes creates a small folder inside the container and puts the secret value in a file there. When the secret changes in AWS and ESO syncs it into Kubernetes, the kubelet quietly updates that file within about 60 seconds. The container reads the file fresh every time it needs the value, so it sees the new password automatically.</p>
<p>Same secret, two paths. One goes stale while one stays fresh.</p>
<p>The problem happens when your app reads the database password from the environment variable, the sticky note, and someone rotates the password in AWS. ESO updates Kubernetes. The file gets the new password. But your app is still reading the sticky note, which has the old one. Connection fails.</p>
<p>That difference isn't a bug. It's how the Linux process model and the kubelet work. Understanding it is the difference between knowing Kubernetes secrets and actually operating them.</p>
<p>Here is what you're about to observe in the lab:</p>
<ul>
<li><p>The rotation script updates the secret in AWS</p>
</li>
<li><p>ESO syncs the new value into Kubernetes within seconds</p>
</li>
<li><p>The volume file updates automatically</p>
</li>
<li><p>The environment variable stays stale until the pod restarts</p>
</li>
<li><p>The <code>/secrets/compare</code> endpoint shows both values side by side so you can see the gap live</p>
</li>
</ul>
<h3 id="heading-step-1-confirm-the-lab-is-ready">Step 1: Confirm the Lab Is Ready</h3>
<p>Make sure your pod and the External Secrets Operator are both running before you start.</p>
<pre><code class="language-bash">kubectl get pods -n external-secrets
kubectl get pods -n default
</code></pre>
<p>Both should show <code>Running</code>.</p>
<h3 id="heading-step-2-run-the-rotation-test-script">Step 2: Run the Rotation Test Script</h3>
<pre><code class="language-bash">bash rotation/test-rotation.sh
</code></pre>
<p>The script performs these actions in order:</p>
<ol>
<li><p>Reads the current <code>DB_PASSWORD</code> from the volume mount at <code>/etc/secrets/DB_PASSWORD</code></p>
</li>
<li><p>Reads the current <code>DB_PASSWORD</code> from the environment variable</p>
</li>
<li><p>Updates AWS Secrets Manager with a new password using <code>put-secret-value</code></p>
</li>
<li><p>Forces an immediate ESO sync by annotating the <code>ExternalSecret</code> with <code>force-sync</code></p>
</li>
<li><p>Reads the volume value again</p>
</li>
<li><p>Reads the environment variable again</p>
</li>
</ol>
<p>After the script runs, the volume and the env var will show different values.</p>
<h3 id="heading-step-3-validate-with-the-compare-endpoint">Step 3: Validate With the Compare Endpoint</h3>
<p>Hit the compare endpoint and look at the output.</p>
<pre><code class="language-bash">curl -s http://localhost:3000/secrets/compare | python3 -m json.tool
</code></pre>
<p>You'll see something like this:</p>
<pre><code class="language-json">{
  "comparison": {
    "DB_PASSWORD": {
      "env": "old-password-value",
      "volume": "new-password-value",
      "match": false
    }
  },
  "all_match": false,
  "rotation_detected": true,
  "message": "Volume has new value; env still has old value."
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/c4ebb09f-e605-4f68-8e12-1361d94199b2.png" alt="Rotation mismatch, the volume file updated with the new password but the env var still holds the old value from pod startup." style="display:block;margin:0 auto" width="832" height="290" loading="lazy">

<h3 id="heading-step-4-restart-the-deployment-to-sync-env-vars">Step 4: Restart the Deployment to Sync Env Vars</h3>
<p>Env vars don't update in place. You need a pod restart so new containers start with the updated Kubernetes Secret.</p>
<pre><code class="language-bash">kubectl rollout restart deployment/myapp -n default
kubectl rollout status deployment/myapp -n default
</code></pre>
<p>Then hit <code>/secrets/compare</code> again. All rows should now show <code>"all_match": true</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/0040274d-a398-408c-9486-ce0a9e527479.png" alt="After a rolling restart, new pods pick up fresh env vars and all keys match." style="display:block;margin:0 auto" width="821" height="436" loading="lazy">

<h3 id="heading-how-to-automate-restarts-with-reloader">How to Automate Restarts With Reloader</h3>
<p>If you don't want to restart deployments manually after every rotation, you can install <a href="https://github.com/stakater/reloader"><strong>Stakater Reloader</strong></a>. It watches an annotation on the <code>Deployment</code> and triggers a rolling restart automatically when the referenced Kubernetes Secret changes. New pods start with fresh env vars, while old pods drain cleanly. The repo's local deployment guide includes the install steps.</p>
<h2 id="heading-how-to-choose-between-external-secrets-operator-and-the-csi-driver">How to Choose Between External Secrets Operator and the CSI Driver</h2>
<p>Two patterns dominate when it comes to pulling external secrets into Kubernetes: the External Secrets Operator and the <a href="https://secrets-store-csi-driver.sigs.k8s.io/">Secrets Store CSI Driver</a>.</p>
<p>Both get cloud secrets into pods, but they do it differently. Here's a plain comparison:</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>External Secrets Operator</th>
<th>Secrets Store CSI Driver</th>
</tr>
</thead>
<tbody><tr>
<td>Creates a Kubernetes Secret</td>
<td>Yes</td>
<td>No by default</td>
</tr>
<tr>
<td>Supports <code>envFrom</code></td>
<td>Yes</td>
<td>No (workaround only)</td>
</tr>
<tr>
<td>Secret stored in etcd</td>
<td>Yes (base64)</td>
<td>No, if you skip sync</td>
</tr>
<tr>
<td>Rotation</td>
<td>ESO updates the Secret, Reloader restarts pods</td>
<td>Volume file can update in place</td>
</tr>
<tr>
<td>Best for</td>
<td>Most teams. Multi-cloud, env var support</td>
<td>Security policies that prohibit secrets in etcd</td>
</tr>
</tbody></table>
<p>This lab uses the External Secrets Operator for two reasons. First, it produces a native Kubernetes Secret, which means your application and deployment patterns match standard Kubernetes workflows. Second, having both <code>envFrom</code> and a volume mount point to the same Secret makes the rotation behavior easy to observe side by side.</p>
<p>Use the CSI Driver when your security team prohibits storing secrets in etcd through a Kubernetes Secret. The driver mounts secret data directly into the pod file system without creating a Kubernetes Secret. The tradeoff is that you lose the native <code>envFrom</code> model.</p>
<h2 id="heading-how-to-deploy-the-pattern-on-amazon-elastic-kubernetes-service">How to Deploy the Pattern on Amazon Elastic Kubernetes Service</h2>
<p>The local lab is ideal for learning. The Amazon Elastic Kubernetes Service path adds the production-like pieces: IAM role-based permissions for the operator, a load balancer for the app, and a full CI/CD workflow.</p>
<h3 id="heading-step-1-prepare-terraform-and-openid-connect-access">Step 1: Prepare Terraform and OpenID Connect Access</h3>
<p>The repository includes a one-time setup guide for OpenID Connect-based access from GitHub Actions to AWS. Run these commands in the <a href="https://github.com/Osomudeya/k8s-secret-lab/tree/main/terraform/github-oidc"><code>terraform/github-oidc</code></a> folder.</p>
<pre><code class="language-bash">cd terraform/github-oidc
terraform init
terraform plan -var="github_repo=YOUR_ORG/YOUR_REPO"
terraform apply -var="github_repo=YOUR_ORG/YOUR_REPO"
terraform output role_arn
</code></pre>
<p>Copy the role ARN from the output. You'll need it in the next step.</p>
<h3 id="heading-step-2-set-the-required-environment-variable">Step 2: Set the Required Environment Variable</h3>
<p>The Amazon Elastic Kubernetes Service spin-up path needs your GitHub Actions role ARN so Terraform can grant the CI/CD runner access to the cluster.</p>
<p>To find your AWS account ID, run:</p>
<pre><code class="language-bash">aws sts get-caller-identity --query Account --output text
</code></pre>
<p>Then set the variable, replacing <code>ACCOUNT</code> with the number that command returns.</p>
<pre><code class="language-bash">export GITHUB_ACTIONS_ROLE_ARN=arn:aws:iam::ACCOUNT:role/your-github-oidc-role
</code></pre>
<h3 id="heading-step-3-run-the-spin-up-script-for-amazon-elastic-kubernetes-service">Step 3: Run the Spin-Up Script for Amazon Elastic Kubernetes Service</h3>
<pre><code class="language-bash">bash spinup.sh --cluster eks
</code></pre>
<p>When the script finishes, it prints the application URL. Open that URL in a browser and confirm that you see the same secrets table you saw locally, with all keys showing <code>Match ✓</code>.</p>
<h3 id="heading-step-4-test-rotation-on-the-deployed-app">Step 4: Test Rotation on the Deployed App</h3>
<p>After you confirm normal operation, run the rotation test the same way you did locally.</p>
<pre><code class="language-bash">bash rotation/test-rotation.sh
</code></pre>
<p>Then use <code>/secrets/compare</code> on the Amazon Elastic Kubernetes Service load balancer URL to validate behavior in the cloud environment.</p>
<p>⚠️ <strong>Cost warning:</strong> Amazon Elastic Kubernetes Service runs at approximately $0.16 per hour. When you're done with the lab, run <a href="https://github.com/Osomudeya/k8s-secret-lab/blob/main/teardown.sh"><code>bash teardown.sh</code></a> from the repo root to destroy all AWS resources and stop charges.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/56f05ace-9ab6-4b67-ade6-a0bd1fa3962c.png" alt="Screenshot of the app running on the ALB URL, showing all keys matched" style="display:block;margin:0 auto" width="912" height="891" loading="lazy">

<h2 id="heading-how-to-configure-github-actions-without-stored-aws-credentials">How to Configure GitHub Actions Without Stored AWS Credentials</h2>
<p>The typical CI/CD setup stores <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> in GitHub repository secrets. Those keys never rotate. Anyone with repo access can read them. When someone leaves the team, you have to revoke keys and update every workflow.</p>
<p>OpenID Connect eliminates that problem entirely.</p>
<h3 id="heading-how-openid-connect-works-for-github-actions">How OpenID Connect Works for GitHub Actions</h3>
<p>GitHub can issue a short-lived token for each workflow run. That token identifies the run: the repository, branch, and workflow name. You create an IAM role in AWS whose trust policy says: only accept requests that come from this specific GitHub repository and branch. The GitHub Actions runner exchanges that token for temporary AWS credentials via <code>AssumeRoleWithWebIdentity</code>. No long-lived keys are ever stored anywhere.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698d563262d4ce66226a844a/48e72210-a669-440e-b42e-81b0c15746ec.png" alt="The full OIDC authentication flow for GitHub Actions deploying to EKS — from minting the JWT token through AssumeRoleWithWebIdentity to temporary credentials, kubeconfig retrieval, and final kubectl apply steps." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h3 id="heading-step-1-create-the-iam-role-with-terraform">Step 1: Create the IAM Role With Terraform</h3>
<p>The <a href="https://github.com/Osomudeya/k8s-secret-lab/tree/main/terraform/github-oidc"><code>terraform/github-oidc</code></a> folder creates the OpenID Connect provider and the IAM role for you. You already ran this in the Amazon Elastic Kubernetes Service setup above. The role ARN is the only value you need to store.</p>
<h3 id="heading-step-2-add-the-role-arn-to-github-repository-secrets">Step 2: Add the Role ARN to GitHub Repository Secrets</h3>
<p>In your GitHub repository:</p>
<ol>
<li><p>Go to Settings → Secrets and variables → Actions</p>
</li>
<li><p>Click New repository secret</p>
</li>
<li><p>Name it <code>AWS_ROLE_ARN</code></p>
</li>
<li><p>Paste the role ARN from the Terraform output</p>
</li>
</ol>
<p>That is the only secret you store. The role ARN isn't sensitive. It's an identifier, not a credential.</p>
<h3 id="heading-step-3-configure-terraform-state">Step 3: Configure Terraform State</h3>
<p>For CI/CD to work consistently across runs, Terraform needs a shared state backend. The lab stores Terraform state in an Amazon S3 bucket and uses an Amazon DynamoDB table for state locking. The Amazon Elastic Kubernetes Service deployment guide in the repo covers the backend setup in full.</p>
<h3 id="heading-step-4-push-to-main-and-let-workflows-run">Step 4: Push to Main and Let Workflows Run</h3>
<p>After your first spin-up, every push to the <code>main</code> branch drives the CI/CD pipeline. The repo includes separate workflow files for Terraform infrastructure changes and application deployment changes. Once your application is reachable, use <code>/secrets/compare</code> to validate rotation behavior on the live environment.</p>
<h2 id="heading-how-to-troubleshoot-the-most-common-failures">How to Troubleshoot the Most Common Failures</h2>
<p>Here's a shortlist of the most common symptoms and their fixes.</p>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Most Likely Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td><code>ExternalSecret</code> is not syncing</td>
<td>Missing credentials or wrong store reference</td>
<td>Confirm the operator can access AWS Secrets Manager and that <code>secretStoreRef</code> points to the correct store</td>
</tr>
<tr>
<td>Pod is stuck in <code>Pending</code></td>
<td>Missing storage setup for local cluster</td>
<td>For Microk8s, enable the storage add-on</td>
</tr>
<tr>
<td>Env and volume still match after rotation</td>
<td>Rotation happened but the pod never restarted</td>
<td>Run <code>kubectl rollout restart</code> or install Reloader</td>
</tr>
<tr>
<td>CRD or API version mismatch</td>
<td>ESO version and manifest <code>apiVersion</code> don't match</td>
<td>Verify the <code>apiVersion</code> for <code>ClusterSecretStore</code> and <code>ExternalSecret</code> match your installed ESO version</td>
</tr>
<tr>
<td>Amazon Elastic Kubernetes Service node group never joins</td>
<td>Networking or IAM permissions for nodes are wrong</td>
<td>Fix internet routing and review the node IAM policy</td>
</tr>
</tbody></table>
<h3 id="heading-how-to-inspect-the-operator-and-the-externalsecret">How to Inspect the Operator and the ExternalSecret</h3>
<p>When something isn't syncing, start with these two commands.</p>
<pre><code class="language-bash"># Check the ExternalSecret status
kubectl describe externalsecret app-db-secret -n default

# Check the ESO operator logs
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
</code></pre>
<p>The status conditions on the <code>ExternalSecret</code> resource will usually tell you exactly what failed.</p>
<h3 id="heading-how-to-validate-rotation-from-the-app-side">How to Validate Rotation From the App Side</h3>
<p>When you are debugging rotation, don't rely only on Kubernetes resource state. Use the <code>/secrets/compare</code> endpoint to see what the running application actually observes. The endpoint tells you whether env and volume match and whether rotation has been detected. That is the ground truth for your application's behavior.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a complete secrets pipeline from AWS Secrets Manager into Kubernetes pods using Terraform and the External Secrets Operator. You ran the local lab, inspected the <code>ExternalSecret</code> and <code>Deployment</code> manifests, and validated that the application sees the right credentials.</p>
<p>You also tested secret rotation and observed the key behavior firsthand: mounted secret files update within the kubelet sync period, while environment variables stay stale until the pod restarts. That single observation explains a large class of production incidents.</p>
<p>Finally, you saw how the same design extends to Amazon Elastic Kubernetes Service with OpenID Connect-based CI/CD, and you have a troubleshooting checklist for the failures most teams hit.</p>
<p>The lab repository is at <a href="https://github.com/Osomudeya/k8s-secret-lab">github.com/Osomudeya/k8s-secret-lab</a>. If you ran the local lab, the natural next step is phases 4 and 5 from the repo's staged learning path: try the CSI driver path on Microk8s, then follow the EKS setup to see the same pipeline with a real CI/CD workflow and no credentials stored in GitHub. Both are documented in the repo and take less than 30 minutes each.</p>
<p>If this helped you, star the repo and share it with someone who is learning Kubernetes.</p>
<p><em>I send weekly breakdowns of real production incidents and how engineers actually fix them, not tutorials but real failures<br>→</em> <a href="https://osomudeya.gumroad.com/subscribe"><em>Join the newsletter</em></a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Securely Deploy APIs to Amazon Lambda – A Practical Guide ]]>
                </title>
                <description>
                    <![CDATA[ Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulner... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-securely-deploy-apis-to-amazon-lambda-a-practical-guide/</link>
                <guid isPermaLink="false">68e8418df4be6f5ede699317</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ aws lambda ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API Gateway ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ secrets management ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Agnes Olorundare ]]>
                </dc:creator>
                <pubDate>Thu, 09 Oct 2025 23:13:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760051580641/75d09121-6167-4e06-94d7-53cf23a6f6a1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulnerable third-party APIs.</p>
<p>Gaps in APIs can occur before requests reach the APIs, within the code housing the APIs, and even along the path of the APIs’ communication with downstream services, dependencies, or other microservices.</p>
<p>Attackers leverage flaws in APIs to gain access to confidential data, harvest or manipulate data, or even make your service unavailable through distributed denial of service attacks.</p>
<p>In this article, you’ll learn to deploy your APIs in Lambda and apply some security measures pre-function, within the function, and post-function.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-an-api">What is an API?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-requirementsprerequisites">Requirements/Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-goal">Project Goal</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-overall-architecture">Project Overall Architecture</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-aws-set-up">AWS Set Up</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clone-project">Clone Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-simple-notification-service">Set Up Simple Notification Service</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-secrets-manager">Set Up Secrets Manager</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-internal-lambda">Set Up Internal Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-external-lambda">Set Up External Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-web-application-firewall">Configure Web Application Firewall</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-cognito-user-pools">Configure Cognito User Pools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-api-gateway">Configure API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-setup-end-to-end">Test Setup End-to-End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clean-up">Clean Up</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-improvements">Improvements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-an-api">What is an API?</h2>
<p>The focus of this article is the security of Application Programming Interfaces (APIs). An API is an interface that connects two programms or applications, allowing them to exchange data and communicate.</p>
<p>An API can be internal to an organization or it can belong to a third-party that allows other users to consume their data through the API.</p>
<h2 id="heading-requirementsprerequisites">Requirements/Prerequisites</h2>
<p>While this tutorial is beginner-friendly, you’ll need the following prerequisites to follow along seamlessly:</p>
<ul>
<li><p>A basic knowledge of the AWS Cloud.</p>
</li>
<li><p>An AWS account with administrator access.</p>
</li>
<li><p>AWS CLI. You can find the installation guide <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">here</a>. Follow the instructions for your operating system.</p>
</li>
<li><p>Python. You can visit Python’s official documentation <a target="_blank" href="https://www.python.org/downloads/">site</a> for a guide on how to download and install Python for your specific operating system.</p>
</li>
<li><p>Pipenv or any Python virtual environment creation tool. You can find the Pipenv installation guide <a target="_blank" href="https://pypi.org/project/pipenv/">here</a><em>.</em></p>
</li>
<li><p>A basic knowledge of Git.</p>
</li>
<li><p>An API client, like Postman or Thunderclient.</p>
</li>
</ul>
<h2 id="heading-project-goal">Project Goal</h2>
<p>By the end of this project, you should be able to deploy APIs in Lambda securely, leveraging AWS cloud-native security services.</p>
<h2 id="heading-project-overall-architecture">Project Overall Architecture</h2>
<p>Below is the architecture of the project workflow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758829544078/b76347ee-bbd3-41f4-88c8-2b3a89ad9087.png" alt="Project Architectural Diagram" class="image--center mx-auto" width="1159" height="464" loading="lazy"></p>
<p>As shown in the architectural diagram, when a user sends a request (a JSON object consisting of the user’s name) to an API hosted in Lambda, the user first gets authenticated by an authentication service called Amazon Cognito.</p>
<p>The request passes through a Web Application Firewall, then an API Gateway. API Gateway will perform a check to see if the user is authorized to access the API using the token that the user sends with the request after authentication. API Gateway then allows the traffic to pass through to the API if the user is authorized.</p>
<p>The user’s request will first get to an External Lambda function, which will then save the user’s name as a message to a Simple Notification Service (SNS) topic. This will then invoke an Internal Lambda to run and log the output in Amazon CloudWatch logs. The SNS topic will be accessed by External Lambda using the SNS’s unique identifier stored in Amazon Secrets Manager.</p>
<h3 id="heading-aws-set-up">AWS Set Up</h3>
<p>You’ll need to set up an AWS environment to get started. This requires creating an account if you don’t already have one.</p>
<p>Following account creation, a root user is automatically created, with all privileges attached to the user. Security best practice is to create another user with administrator privileges and use this user for subsequent tasks.</p>
<p>Then, create an access key for this user, which usually consists of two parts (Access Key ID and Secret Access Key) by navigating to the following:</p>
<p>IAM —→ Users —→ Create Access key</p>
<p>Follow the prompts and choose the <code>Command Line Interface</code> option. Check the <code>Confirmation</code> box, and go on to create the key. Download the CSV file provided, or manually copy the <code>Access Key ID</code> and <code>Secret Access Key</code>. Save them securely.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890397608/a88ec1c6-511c-4a66-aa7a-d0dd3f41f665.png" alt="IAM Dashboard" class="image--center mx-auto" width="951" height="513" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890497481/faab2cb7-b7ba-4e5c-b67e-a00d8fb27a10.png" alt="IAM User Page" class="image--center mx-auto" width="1366" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890928429/1a4b3163-6340-47d2-be3e-0e61c275ba8f.png" alt="Create Access Key Page" class="image--center mx-auto" width="1348" height="551" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890991928/5bb150b6-b014-4398-b839-ee5d6e49c425.png" alt="Access Key Use Option Page" class="image--center mx-auto" width="1366" height="494" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891021874/a2e4eb61-eaca-4732-9377-b499fa7eab5d.png" alt="Set Access Key Tag Page" class="image--center mx-auto" width="1366" height="348" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891049362/372f3a63-8e64-478d-9b06-61f7aa88f73a.png" alt="Download Access Key" class="image--center mx-auto" width="1366" height="517" loading="lazy"></p>
<p>Open up your terminal and run the following commands using the AWS CLI:</p>
<pre><code class="lang-bash">aws configure
</code></pre>
<p>The above command will give some prompts to provide the components of the <code>Access Key</code> created earlier and your default region (the AWS region hosting the service you intend to interact with).</p>
<h3 id="heading-clone-project">Clone Project</h3>
<p>In the next step, you’ll clone the GitHub repository containing the assets and resources used in the project implementation.</p>
<p>Visit the project <a target="_blank" href="https://github.com/Agnes4Him/secure-lambda">URL</a> and clone the repository locally.</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;repository_clone_url&gt;
</code></pre>
<h3 id="heading-set-up-simple-notification-service">Set Up Simple Notification Service</h3>
<p>Amazon Simple Notification Service (SNS) connects system components, enabling asynchronous communication and messaging among them.</p>
<p>Find <code>SNS</code> on the console, click on it, and create a topic that your APIs will send messages to. After successfully creating a topic, navigate to the topic, and in the topic details, you’ll find the topic’s <code>ARN</code>. An ARN is an Amazon Resource Name, and it’s a unique string attached to a resource you’ve created on AWS to help identify the resource. Copy the <code>ARN</code> of the topic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983690093/a2820581-46fb-41d1-aed9-9471a0c2db02.png" alt="SNS Dashboard" class="image--center mx-auto" width="1349" height="517" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758982964553/3eb717c7-8ce3-497b-96fb-c16483cff43e.png" alt="Create SNS Topic" class="image--center mx-auto" width="1366" height="371" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983004729/854335ec-3d53-42e4-8bef-0e8a7d3fb2e6.png" alt="Topic Details" class="image--center mx-auto" width="1348" height="541" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983094356/320ebaf9-9f2d-4241-b747-fd3fd0f0b62b.png" alt="SNS Topic Access Policy" class="image--center mx-auto" width="1348" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983451385/7886c2c4-a52f-4538-8e9d-0d33738f7632.png" alt="Topic Created" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<h3 id="heading-set-up-secrets-manager">Set Up Secrets Manager</h3>
<p>Amazon Secrets Manager is used to store, manage, and retrieve sensitive information such as keys, credentials, tokens, and so on. You’ll store the <code>Topic ARN</code> created earlier. With this approach, you’ll demonstrate how your API can securely access the data and information it needs for its performance.</p>
<p>Go to <code>Secrets Manager</code> on the AWS console and create a secret. Provide the secret’s details, and add a new secret named <code>TOPIC_ARN</code> as the key and the actual SNS Topic ARN as the value.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984342157/38c7855c-2221-4406-9078-496cbb480e47.png" alt="Secrets Manager Console" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984379959/3c4ebdf7-26c8-4b74-a175-0ea2eefd258d.png" alt="Create Secret" class="image--center mx-auto" width="1351" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984459477/cfdd8fce-4a33-45c3-8f12-d6a4a04bf799.png" alt="Choose Other Types of Secret" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984512368/7a76618e-18f6-4b3d-bc03-5941c89909ef.png" alt="Secret Details" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984575543/91ca9360-6320-442a-a121-37f9f35b175b.png" alt="Final Secret Store" class="image--center mx-auto" width="1356" height="314" loading="lazy"></p>
<p>Next, you’ll create some Lambda functions to serve your APIs and consume the output of the APIs. There’re three Lambda functions to set up. Two of the functions will host APIs, each of which can only be accessed by specific users. These will be referred to as <code>ExternalLambda</code>. The third Lambda will consume the output of the External Lambda functions through SNS.</p>
<h3 id="heading-set-up-internal-lambda">Set Up Internal Lambda</h3>
<p>AWS Lambda is a serverless service on AWS that users can leverage to run application functions or code when needed. You’re billed for your Lambda function based on the number of invocations of the function, the duration each invocation lasted, and the amount of memory allocated to the function. Lambda can be provisioned to use any runtime, such as Python or NodeJS. In this demonstration, you’ll focus on the NodeJS runtime.</p>
<p>Now that you know what Lambda is and does, you can create one. Let’s call the first Lambda function InternalLambda. On the AWS console, search for <code>Lambda</code>, and on the <code>Lambda</code> dashboard, click <code>Create a function</code> and provide the details. We’ll be using <code>Node.js</code> – JavaScript at the backend as the runtime of choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140048151/60d5c813-a190-456e-9bad-50b429bdc6f7.png" alt="AWS Lambda" class="image--center mx-auto" width="1351" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140165012/d519f8e9-cb98-4f75-b94e-d4d343e3003c.png" alt="Lambda Details" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p>For the <code>Permissions</code> details, let Lambda create a default <code>IAM Role</code>. This default role is named according to your function, and the permissions attached to the role allow your Lambda function to send logs to CloudWatch, another AWS service used for monitoring and observability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140368576/3f922a46-7b3c-4034-b20b-c2b9ab5dde94.png" alt="Lambda Permissions" class="image--center mx-auto" width="1348" height="528" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146593382/39d50020-d0bf-4cfe-950f-eec8a2ff8989.png" alt="39d50020-d0bf-4cfe-950f-eec8a2ff8989" class="image--center mx-auto" width="1345" height="546" loading="lazy"></p>
<p>As you can see in the last image above, the Lambda function you’ve created needs a <code>trigger</code> and sometimes, a <code>destination</code>. For your <code>InternalLambda</code>, the trigger is the SNS topic we configured earlier. This Lambda will read the messages that’ve been published to it, and then you can access the message from your client or even CloudWatch logs.</p>
<p>To achieve this, click the <code>Add trigger</code> button and provide the details.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140925997/2534535f-5ad3-4e13-99f1-d8bf48c9cec1.png" alt="Add SNS to Lambda" class="image--center mx-auto" width="1358" height="508" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146639798/1f3d75c9-ef5d-4538-9f3a-aaf2e8c0ddbb.png" alt="SNS ARN" class="image--center mx-auto" width="1366" height="533" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146670136/367f7ca9-0b41-41ed-8749-ff70e1770ebb.png" alt="InternalLambda Overview" class="image--center mx-auto" width="1339" height="535" loading="lazy"></p>
<p>Next, you’ll provide the <code>code</code> you want to invoke through Lambda. Find the code in the GitHub repository that you cloned earlier. Paste the code in the Lambda function code space and click on <code>Deploy</code> to deploy the function.</p>
<p><code>secure-lambda/InternalLambda/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request successfully received from SNS'</span>);                            

        <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'Records'</span>][<span class="hljs-number">0</span>][<span class="hljs-string">'Sns'</span>][<span class="hljs-string">'Message'</span>];
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from InternalLambda!`</span>),
        };       
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response: '</span>, response);                                                
        <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (err) {                            
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occurred while processing your request.'</span>),
        };

        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error processing event'</span>, err);
        <span class="hljs-keyword">return</span> response;
    }   
};
</code></pre>
<p>The function defined in the index.js file above is simply taking the <code>event</code> object sent to it from SNS and extracting the <code>Message</code> attribute within it. We’re using <code>console.log</code> here to view outputs from the function and ensure it behaves as expected. Just don’t use this in a production-ready application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759142277747/f4437ff2-4495-485d-b891-d9dda3fc939c.png" alt="InternalLambda Code" class="image--center mx-auto" width="1342" height="550" loading="lazy"></p>
<h3 id="heading-set-up-external-lambda">Set Up External Lambda</h3>
<p>You’ll be creating two external Lambda functions: 1 and 2. These two functions will receive user requests, process them, and publish messages to your SNS topic.</p>
<p>On the Lambda console, create another function and name it <code>ExternalLambda1</code>. Allow Lambda to create a default IAM Role, as previously.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759144966306/ee8a2ed1-5a2e-48df-8556-5dedd7ecdde1.png" alt="Create ExternalLambda1" class="image--center mx-auto" width="1351" height="539" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146732803/82a46fd1-e3e5-4d72-a9fe-b41496ba076b.png" alt="ExternalLambda1 Overview" class="image--center mx-auto" width="1345" height="546" loading="lazy"></p>
<p>Paste the code snippet below in the <code>ExternalLambda1</code> code space:</p>
<p><code>secure-lambda/ExternalLambda1/insex.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);                             <span class="hljs-comment">// added for debugging</span>
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda1! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }  
};
</code></pre>
<p>The code above leverages the AWS SDK to fetch the ARN of the SNS topic created earlier from Secrets Manager. It then publishes a message to the topic.</p>
<p>The SDK already comes installed in the Lambda function. Outside of Lambda, the SDK has to be explicitly installed. The function receives its <code>event</code> from the client via API Gateway, which we’ll configure later.</p>
<p>The SNS topic you created earlier will be the destination for this function. For Lambda to publish a topic to SNS, it needs the necessary permission attached to its IAM Role. AWS can automatically take care of that during your configuration, as shown below.</p>
<p>For the trigger, you’ll use another service known as <code>API Gateway</code>. More on that later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146816330/3c542eff-984e-4d02-85b3-c1da142f94d7.png" alt="ExternalLambda1 Add Destination" class="image--center mx-auto" width="1345" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146778567/9b3650f1-90fd-47ce-99c3-627654f2d41f.png" alt="ExternalLambda1 Destination Permissions" class="image--center mx-auto" width="1348" height="539" loading="lazy"></p>
<p>Follow the same steps to provision another Lambda known as <code>ExternalLambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759145915181/631aa639-493a-4f12-af1e-45f425ca2c16.png" alt="ExternalLambda2" class="image--center mx-auto" width="1351" height="539" loading="lazy"></p>
<p>The outcome of the External Lambda setup is as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146919917/93aaf281-387f-44c3-bf6d-995b076150e9.png" alt="ExternalLambda2 Overview" class="image--center mx-auto" width="1346" height="539" loading="lazy"></p>
<p>Paste the code below into <code>ExternalLambda2</code>. It performs the same function as <code>ExternalLambda1</code>, but their output differ. Each of the two Lambda functions will be receiving traffic to a specific user that’s authorized to access the function.</p>
<p><code>secure-lambda/ExternalLambda2/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);  
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda2! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }              
};
</code></pre>
<p>Before moving on, you need to modify the External Lambda’s IAM Roles. Currently, IAM Roles only have permissions to write to CloudWatch and SNS (automatically added). External Lambda also needs permission to fetch the ARN of the SNS topic that was created earlier.</p>
<p>The point here is to show how to leverage a secrets manager, such as AWS Secrets Manager, to store sensitive information or data, and still access these securely. This approach is more secure than storing the ARN as an environment variable within Lambda.</p>
<p>Navigate to IAM, and click on <code>Policies</code> tab on the left. This brings you to a list of policies. Next, click on <code>Create policy</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320098124/71bde9ad-c6d9-4c0d-8472-d56107708be2.png" alt="IAM Policies" class="image--center mx-auto" width="1348" height="542" loading="lazy"></p>
<p>Search for <code>secrets manager</code> in the Policy editor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320163875/a040af9a-1e92-4aea-8c2e-5c029f60f54e.png" alt="Policy Editor" class="image--center mx-auto" width="1366" height="539" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320923537/c3e8bb54-e78d-498c-9fec-7c7a5225b116.png" alt="Policy Editor2" class="image--center mx-auto" width="1366" height="546" loading="lazy"></p>
<p>Select the permissions Lambda needs to access Secrets Manager. In this case, that would be <code>Read —&gt; GetSecretValue</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321060458/963c1876-dbcc-4d7d-a6fe-9f65281c1a26.png" alt="Policy Editor - Specify Permissions" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p>Select <code>Specific</code> for Resources, and click on <code>Add ARNs</code>. On the next tab, add the details of the Secrets Manager Secret created earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321219657/f50fc354-a238-499c-9009-958bbc624299.png" alt="Policy Editor - Select Access" class="image--center mx-auto" width="1348" height="532" loading="lazy"></p>
<p>The Secret’s ARN will be populated here.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321662642/356fb6bd-adc7-4663-a337-3cfaedb74b2d.png" alt="Policy Editor - Add Secrets Manager ARN" class="image--center mx-auto" width="1351" height="550" loading="lazy"></p>
<p>Next, give the policy a name and create it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321510186/fa5da448-293f-4d95-a3b5-651292a91a7f.png" alt="Policy Editor - Create Policy" class="image--center mx-auto" width="1342" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321721882/27807dda-8ea3-4489-bcab-d03efc201655.png" alt="Newly Added Policy" class="image--center mx-auto" width="1354" height="546" loading="lazy"></p>
<p>Next, navigate to <code>Roles</code>, and search for the IAM Roles assigned to the External Lambda functions. These are named according to the Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321748368/dfc73acb-622c-44f9-9cf8-be51b31e3fe9.png" alt="IAM Roles" class="image--center mx-auto" width="1351" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321856410/f1d4a13c-a568-4c9c-b14f-bb3d24b870f8.png" alt="Lambda IAM Roles" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p>Click <code>Add permissions</code> to add a new permission to the IAM Role selected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322020293/689715ef-7e8c-45cb-9473-010f5aa105fa.png" alt="ExternalLambda1 Role" class="image--center mx-auto" width="1352" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322453532/83996cca-7a05-48fb-8e31-d3fc679df7bc.png" alt="ExternalLambda1 Role - Policy Added" class="image--center mx-auto" width="1349" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322498243/29a8fff5-af9a-4d4c-b3ff-3790b82b6339.png" alt="ExternalLambda2 Role" class="image--center mx-auto" width="1352" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322570298/ab28750d-eb99-40f3-bf48-936b63bba1f0.png" alt="ExternalLambda2 Role - Policy Added" class="image--center mx-auto" width="1350" height="553" loading="lazy"></p>
<h3 id="heading-configure-web-application-firewall">Configure Web Application Firewall</h3>
<p>A firewall is a system placed in front of an application, workload, APIs, and so on to inspect traffic, filter it, and either allow or block the traffic based on some preconfigured rules.</p>
<p>For this project, you’ll use the AWS Web Application Firewall (WAF) service to inspect user requests before routing the traffic to your APIs running in Lambda.</p>
<p>Head over to the AWS console and search for WAF.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759310730367/bbcbdf00-2759-4dd7-9b7b-63ee9c252542.png" alt="AWS Web Application Firewall" class="image--center mx-auto" width="1353" height="546" loading="lazy"></p>
<p>Click on the <code>IP sets</code> tab on the left. This will enable you to create a list of IP addresses that you want to allow (as in this case) or deny.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311298551/4537577d-a574-417e-8352-3f72b3732926.png" alt="IP Sets Page" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311354043/edd29c9c-63e7-4bf6-a503-23ef0af5ac20.png" alt="IP Set Configuration" class="image--center mx-auto" width="1351" height="549" loading="lazy"></p>
<p>The IP addresses should include a CIDR block. For instance, if adding a single IP address, it should be <code>X.X.X.X/32</code>. The same applies to IP address ranges such as <code>X.X.X.X/24</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311560565/0ad16e51-b70b-4a80-98f4-821659fa61b8.png" alt="IP Set Overview" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p>Next, click on the <code>Web ACLs</code> tab, then <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311623780/9742ab87-3303-4046-84df-f9f770ed7c41.png" alt="Web ACL Page" class="image--center mx-auto" width="1366" height="549" loading="lazy"></p>
<p>Choose <code>Regional resources</code> as the Resource type, and enter your region. It’s best to keep all resources you’re creating in this project within the same region. Give your Web ACL a name, then click next.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311736091/b4201885-2dc0-4ed8-aa38-5e25824c363b.png" alt="Web ACL Description" class="image--center mx-auto" width="1351" height="544" loading="lazy"></p>
<p>Add rules to the Web ACL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311892739/5efad662-6c20-4678-b490-54fa33bc3a7b.png" alt="WAF Rule" class="image--center mx-auto" width="1352" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311985197/9e7157c8-bfb4-47a9-a850-67ce8bb302b2.png" alt="Add Rule" class="image--center mx-auto" width="1350" height="551" loading="lazy"></p>
<p>Choose a rule type. In this case, you’ll use <code>IP set</code>, and give the rule a name. Choose the IP set created earlier.</p>
<p>Select <code>Source IP address</code>, and <code>Count</code> as the Action. For this project, you’ll focus on counting the requests sent to your APIs. But as shown in the image below, you can perform other actions, such as allow, block, and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759312925911/ea491527-970c-4b5f-b658-4345ce3d08e4.png" alt="WAF Rule Configuration" class="image--center mx-auto" width="1366" height="548" loading="lazy"></p>
<p>Your final rule configuration will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313133810/8e1be6d3-f6bf-42d9-881d-87216862b3bd.png" alt="WAF Rule Overview" class="image--center mx-auto" width="1350" height="553" loading="lazy"></p>
<p>Scroll down, then click on <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313210947/d625c4f3-a5a3-47c9-961f-ca67f652c992.png" alt="Create Rule" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313261493/3bd16ec5-3376-4607-86cc-fd1716ad68aa.png" alt="Web ACL Dashboard" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<h3 id="heading-configure-cognito-user-pools">Configure Cognito User Pools</h3>
<p>Amazon Cognito is an identity management service used for creating and managing users. You can leverage it to authenticate and authorize users to applications, APIs, or other workloads.</p>
<p>You’ll create <code>User Pools</code> within Cognito and add a user to each pool. You’ll configure how these users can be authenticated and authorized to access the External Lambda functions already created.</p>
<p>Search for <code>Cognito</code> on AWS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315681568/c2e7df4e-0e51-4c03-bf59-41ca895df74d.png" alt="Amazon Cognito" class="image--center mx-auto" width="1356" height="542" loading="lazy"></p>
<p>Click on <code>Get started for free</code>, then <code>Create user pool</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315735324/1c09e934-186f-49db-811f-dd84d7400285.png" alt="Create User Pool" class="image--center mx-auto" width="1366" height="554" loading="lazy"></p>
<p>Select Single-page application (SPA), give the User pool the name <code>MyUserPool1</code>, and select <code>Email</code> as an option for sign-in. This means the main attribute users will provide at signup and sign-in will be their email address. Leave everything else as the default. We’ll keep things as simple as possible.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315828576/73cb66a3-cbde-4443-8dfd-34338091aabc.png" alt="Use Pool Configuration" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315901551/9fdc173d-f92b-4080-98d4-513b404a9aeb.png" alt="User Pool Configuration2" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315994247/3fb17ade-90aa-4d47-b2f9-258f6b547a1f.png" alt="User Pool Configuration3" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p>After creating the User pool, you’ll find the page shown below. You can view the login and signup page for the pool you’ve just created by clicking on the <code>View login page</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316208497/4db5f370-deb2-449e-8017-505dc1e13079.png" alt="Cognito App Client Login URL" class="image--center mx-auto" width="1352" height="551" loading="lazy"></p>
<p>You can add <code>App clients</code> to your User Pool. By default, a client named <code>MyUserPool1</code> will be added to the pool. Navigate to your User pool, and click on <code>App clients</code> to see details of this client. Note the <code>Client ID</code>. You’ll also make some edits to the App client by clicking on the <code>Edit</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316443170/58081c40-cdcb-4af9-a60b-79156f6d2d68.png" alt="User Pool App Client Overview" class="image--center mx-auto" width="1348" height="551" loading="lazy"></p>
<p>You’ll edit the <code>Authentication flows</code> field by ticking the <code>Sign in with username and password…</code> and <code>Sign in with server-side administrative credentials…</code> boxes. These changes will enable you to authenticate the user who will be added to this client programmatically, rather than through a UI. With this approach, we can fetch the token assigned to the user by Cognito and use this token to authorize access to Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759317429341/f8db3816-c603-49fe-b661-696bfff98639.png" alt="Edit App Client" class="image--center mx-auto" width="1349" height="549" loading="lazy"></p>
<p>Now, add a user to this pool. The user needs a valid email address. You’ll need the login page URL to create the user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318555729/d9a8ac0c-72d4-4fca-8a94-ff71a5a20caf.png" alt="Cognito Create New User" class="image--center mx-auto" width="1347" height="636" loading="lazy"></p>
<p>You need access to the email used to create the user. Fetch the code sent to the email address and submit to confirm the account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318672240/42d4418d-a4e1-4af9-b8b5-deaf5fb63118.png" alt="Cognito Confirm Email" class="image--center mx-auto" width="1345" height="603" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318710760/4505260f-13c9-4b3b-af99-1cb9e7436147.png" alt="Cognito Successful Sign up" class="image--center mx-auto" width="1366" height="636" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318734000/1b3789d9-917a-4341-84ef-b8e498628557.png" alt="User Pool Users" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Follow the same steps and create another User pool named <code>MyUserPool2</code>. Add a user with a different email to this pool.</p>
<h3 id="heading-configure-api-gateway">Configure API Gateway</h3>
<p>API Gateway is a service used to manage access and route traffic to API backend services such as APIs. It serves as a reverse proxy and provides an extra layer of security for backend services.</p>
<p>You’ll configure API Gateway to direct traffic to your Lambda functions.</p>
<p>Navigate to <code>API Gateway</code> and click on <code>Create an API</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336384538/0e2e4120-ade3-43c3-8a3e-9ec4d0e2b343.png" alt="API Gateway" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p>Select the <code>REST API</code> option —→ <code>Build</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336533762/56d96fcb-ff27-4f96-b739-fc9658dca50e.png" alt="Select API Type" class="image--center mx-auto" width="1352" height="551" loading="lazy"></p>
<p>Select <code>New API</code>, provide a name, and choose <code>Regional</code> as the API endpoint type. IP address type can be IPv4 or Dualstack. We’ll select IPv4 here. Then create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336585590/ff2b11c2-cb32-4657-913e-6dfc9922531f.png" alt="API Gateway Configuration" class="image--center mx-auto" width="1349" height="549" loading="lazy"></p>
<p>An important part of API Gateway configuration for this project is the Authorizer. API Gateway uses Authorizer to allow traffic from clients to backend services.</p>
<p>You’ll create two Authorizers. Each will be connected to one of the User pools you configured earlier. On the left-hand side of the API Gateway you configured, click on <code>Authorizers</code> —→ <code>Create authorizer</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336744757/bb00a260-3897-4867-96d3-5370e40eae59.png" alt="API Gateway Authorizer" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Provide the name <code>AGAuthorizer1</code>, and select <code>Cognito</code> as the Authorizer type. Add the User pool for MyUserPool1 created earlier. For the Token source, use <code>Authorization</code>. When you send a request from your API client, a token will be added to the request header for authorization. The token’s key will be named <code>Authorization</code>, while the value will be the token itself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337066816/1ef4b11c-3508-49a5-ae7b-561b4c7f4259.png" alt="Authorizer1 Configuration" class="image--center mx-auto" width="1366" height="548" loading="lazy"></p>
<p>Create another Authorization for MyUserPool2 the same way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337472200/aacad54f-8927-4c3e-9a60-f207bbf45577.png" alt="Authorizer2 Configuration" class="image--center mx-auto" width="1366" height="544" loading="lazy"></p>
<p>Both Authorizers will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337540381/56362d9c-7f84-4021-b077-59992abd979b.png" alt="Authorizers Overview" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Next, you’ll create resources and endpoints within the API Gateway that you’ve defined.</p>
<p>A <code>resource</code> in API Gateway is used to group certain endpoints within a specific path. You’ll define two resources within the API Gateway you’ve created. This will create two different paths, &lt;BASE_URL&gt;/&lt;RESOURCE1&gt; and &lt;BASE_URL/RESOURCE2&gt;.</p>
<p>On the API Gateway dashboard, navigate to your Gateway, click on <code>Create resource</code>, define your root path (‘/’ in your case), and provide the resource name (<code>lambda1</code>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420480646/c6e7c1c9-9eee-4dbf-af5b-8c335e14927c.png" alt="API Gateway Lambda1 Resource" class="image--center mx-auto" width="1366" height="528" loading="lazy"></p>
<p>Create another resource named <code>lambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420822042/74d1790e-c752-490c-883f-b42bd00d91eb.png" alt="API Gateway Resources Overview" class="image--center mx-auto" width="1366" height="532" loading="lazy"></p>
<p>Now, click on <code>/lambda1</code>, then <code>Create method</code> to define an endpoint within this resource. You’ll use the <code>POST</code> method to send requests to the backend service via this endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420769955/bd861a6a-884d-4873-aaeb-dc958a0915b1.png" alt="API Gateway Method Configuration" class="image--center mx-auto" width="1349" height="541" loading="lazy"></p>
<p>For the backend service or Integration type, select Lambda function, and provide the ARN of ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421208532/8d84c272-58ad-4ca4-ae56-682151495b76.png" alt="API Gateway Method Configuration2" class="image--center mx-auto" width="1352" height="538" loading="lazy"></p>
<p>For Authorization, select <code>AWS IAM —→ Cognito user pool authorizers —→ AGAuthorizer1</code>. Leave other configurations, then create the endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421234684/1e76c46e-ba07-4178-94b3-e579ed752278.png" alt="API Gateway Method Configuration3" class="image--center mx-auto" width="1352" height="542" loading="lazy"></p>
<p>Repeat the same step to create a <code>POST</code> method for <code>/lambda2</code> resource. The <code>method</code> should be attached to <code>ExternalLambda2</code>, and <code>AGAuthorizer2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421691530/563c85e5-52ff-43fa-8216-41fc269989e0.png" alt="API Gateway Deployment" class="image--center mx-auto" width="1349" height="539" loading="lazy"></p>
<p>The API Gateway you’ve created needs to be deployed to become accessible. Deployment is usually done to a Stage.</p>
<p>Click on <code>Deploy API</code>, select New stage and name the stage development. Then, deploy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421954217/f0ca31c8-78d9-4b1b-a47c-424f6ef32093.png" alt="API Gateway Stage" class="image--center mx-auto" width="1366" height="541" loading="lazy"></p>
<p>After deployment to a stage, an invoke URL will be provided. This will serve as the base URL for the endpoints you’ve defined.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422031311/c7cac4e7-52e9-43dd-a565-46aa062aa364.png" alt="API Gateway Stage Overview" class="image--center mx-auto" width="1345" height="539" loading="lazy"></p>
<p>The stage you’ve created needs some modifications for enhanced security. Firstly, you need to attach the <code>WAF</code> that you created earlier. Secondly, the default rate limit for the API deployed to this stage is 10000. Rate limit restricts excessive resource consumption and protects your API from abuse. For this project, you can reduce the limit to 50.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422132101/b0f67d30-30ec-4d1f-9eee-735dc3b26500.png" alt="Edit API Gateway Stage" class="image--center mx-auto" width="1352" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759423441047/95dc8f74-c71d-449a-b15a-46caa53c7595.png" alt="API Gateway Stage - Add Rate Limit and WAF" class="image--center mx-auto" width="1352" height="536" loading="lazy"></p>
<p>To test the API Gateway set up, click on the endpoint you want to test, then the <code>Test</code> button. This initial test doesn’t need any authorization, since the test is done directly within the Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435394293/8933349c-3e2a-4795-adb4-bb9eeb990e81.png" alt="API Gateway Endpoint Testing" class="image--center mx-auto" width="1346" height="549" loading="lazy"></p>
<p>Add JSON data as the Request body. The key will be <code>name</code>, and the value will be any string.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435424778/a613816e-8b89-4978-9b3e-6734e48119eb.png" alt="API Gateway Testing2" class="image--center mx-auto" width="1351" height="541" loading="lazy"></p>
<p>The response sent back from ExternalLambda1 shows a status code of 200, and a response body containing exactly the message expected from the Lambda function.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435744694/516bab58-99de-4b41-8926-e9928b8c42e4.png" alt="API Gateway Test Response" class="image--center mx-auto" width="1349" height="540" loading="lazy"></p>
<p>If you head over to CloudWatch Log groups, you should also find the Log groups that were automatically created for the Lambda functions. Click on the Log group for ExternalLambda1 and navigate to the latest Log stream. You should find the logs for the request you’ve just made from API Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435884121/05741f0c-82dd-43f1-855c-157df5c112fc.png" alt="CloudWatch Logs for Testing" class="image--center mx-auto" width="1366" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436073425/d4a0a2c7-8d86-44ce-adb7-4c67c3cdf40b.png" alt="CloudWatch Logs for Testing2" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436150374/5fae1a11-c80d-41df-8ea9-39303287144b.png" alt="CloudWatch Logs - Output from InternalLambda" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<h3 id="heading-test-setup-end-to-end">Test Setup End-to-End</h3>
<p>To test our setup properly, and from the internet, send the same request from your API client with no additional information in the request header. This should return a <code>401</code> error – Unauthorized. This is expected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436452961/342e7659-5a58-45a2-af26-a657b622a83a.png" alt="Request without Token" class="image--center mx-auto" width="1321" height="448" loading="lazy"></p>
<p>API Gateway expects an authorization token from each request it receives before routing traffic to the appropriate backend service. It validates this token through Cognito.</p>
<p>You’ll mimic a user login for each user added to Coginito User pools to get a token for the user. This token will then be sent alongside any request. To achieve this, you’ll use the two Python scripts I’ve provided below:</p>
<p><code>secure-lambda/auth-scripts/user1.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> boto3

client = boto3.client(<span class="hljs-string">"cognito-idp"</span>)

response = client.initiate_auth(
    AuthFlow=<span class="hljs-string">"USER_PASSWORD_AUTH"</span>,  <span class="hljs-comment"># or ADMIN_USER_PASSWORD_AUTH if using admin creds</span>
    AuthParameters={
        <span class="hljs-string">"USERNAME"</span>: <span class="hljs-string">""</span>,             <span class="hljs-comment"># user1 email</span>
        <span class="hljs-string">"PASSWORD"</span>: <span class="hljs-string">""</span>              <span class="hljs-comment"># user1 password</span>
    },
    ClientId=<span class="hljs-string">""</span>                     <span class="hljs-comment"># Cognito App Client ID</span>
)

id_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"IdToken"</span>]
access_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"AccessToken"</span>]
refresh_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"RefreshToken"</span>]

print(<span class="hljs-string">"ID Token:"</span>, id_token)
</code></pre>
<p>Using the Python boto3 library, you’ll initiate an authentication request to Cognito. Provide the email address and password of the user in MyUserPool1. Also, add the Client ID of the App client.</p>
<p>To run the script, create an isolated environment using Pipenv, uv, or a similar library. Install the dependency used in the project as defined in the Pipfile, and run the script with the Pipenv shell.</p>
<pre><code class="lang-bash">pipenv install
pipenv shell
Python secure-lambda/auth-scripts/user1.py
</code></pre>
<p>The Python command will return with a token assigned to the user. Next, you use this token to authorize a user to access ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759437804885/f03ab150-74f9-4ecf-9c7c-0ee07e8662a2.png" alt="Add Token to Request Header" class="image--center mx-auto" width="711" height="446" loading="lazy"></p>
<p>Ensure that the URL for the POST request is in the format: &lt;BASE_URL/lambda1&gt;. You should receive a response from API Gateway indicating success.</p>
<p>Now try accessing ExternalLambda2 using User1 token. You should get an <code>Unauthorized</code> message. Note that user1 will always receive an unauthorized message when it tries accessing ExternalLambda1 without an Authorization token in the header, a wrong token, or when it tries accessing ExternalLambda2, which it is not authorized to access.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438020972/39e2650a-6a99-466a-ad40-6bcf72c491c4.png" alt="User1 Access ExternalLambda2" class="image--center mx-auto" width="1323" height="439" loading="lazy"></p>
<p>Repeat the process with User2 using the token generated for the user in MyUserPool2. First, test access to ExternalLambda2 without a token in the request header.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438058029/37dbffcb-7f58-4ce0-abfa-abf0e6d1d3a4.png" alt="User2 Request without Token" class="image--center mx-auto" width="1366" height="450" loading="lazy"></p>
<p>Then test access with the token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438118097/a408bfe0-6e94-46a1-b98e-c959f31673f8.png" alt="User2 Request with Token" class="image--center mx-auto" width="1311" height="460" loading="lazy"></p>
<p>Next, try accessing ExternalLambda1 using User2.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438144636/06ba905d-ff7b-48d6-8e5b-e35b959221ba.png" alt="User2 Access ExternalLambda1" class="image--center mx-auto" width="1366" height="466" loading="lazy"></p>
<p>You can also view the outcome of some of the requests made by your client on CloudWatch Logs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438188476/2e5aaa35-b52c-4fd3-88f4-00bc704cd809.png" alt="CloudWatch Logs Output" class="image--center mx-auto" width="1366" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438223720/630bd443-41d2-40b7-b28f-ff937fbf13f9.png" alt="CloudWatch Logs Output2" class="image--center mx-auto" width="1366" height="528" loading="lazy"></p>
<p>Also, since WAF has been configured previously to count requests (although, in a real scenario, you want to achieve much more with WAF, such as allow or block certain traffic), you can view activities captured by WAF by navigating to the service on AWS, then searching for the WAF you configured, and navigating to Traffic overview.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438469300/df718ef7-7eaa-49ad-8780-c43878d2d388.png" alt="WAF - Traffic details" class="image--center mx-auto" width="1366" height="541" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438498828/b1619abf-db45-4946-85e2-53e5a769cdb8.png" alt="WAF - Traffic Details2" class="image--center mx-auto" width="1351" height="541" loading="lazy"></p>
<p>You can find other details, such as the client device types and where requests originated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438552359/477c4f39-04e4-4427-a2d2-74a6066622dd.png" alt="WAF - Traffic Details3" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438590513/31acecd0-7f45-4fb9-b352-5dc5fcf49e75.png" alt="WAF - Traffic Details4" class="image--center mx-auto" width="1348" height="547" loading="lazy"></p>
<h3 id="heading-clean-up">Clean Up</h3>
<p>It’s important to clean up the resources created so far after the hands-on exercise. Due to the dependencies among the resources, trying to delete a resource that another resource depends on may lead to an error. So, you should delete them in this order:</p>
<ul>
<li><p>Secrets Manager</p>
</li>
<li><p>Cognito – Users, App Client, then User Pool</p>
</li>
<li><p>API Gateway – Endpoints/ Methods, Resources, API, Stage</p>
</li>
<li><p>Web Application Firewall – IP Set, Web ACL</p>
</li>
<li><p>All Lambda Functions</p>
</li>
<li><p>Lambda IAM Roles and the policies attached to them</p>
</li>
<li><p>CloudWatch Log Group for all the Lambda functions</p>
</li>
<li><p>SNS Topic</p>
</li>
</ul>
<p>Also, you can deactivate or delete the credentials created for your IAM Admin user if not in use.</p>
<h2 id="heading-improvements">Improvements</h2>
<p>Consider the following areas to improve, apply best practices to, and enhance the security posture of your systems further.</p>
<ol>
<li><p>Use of API keys</p>
</li>
<li><p>Third-party API consumption</p>
</li>
<li><p>API inventory management/ documentation</p>
</li>
<li><p>Resource provisioning using Infrastructure as Code</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Security at every layer of an IT system is not negotiable. In this project, we’ve demonstrated how to leverage cloud-native solutions to secure APIs hosted in a serverless service, allowing only authorized users access to the APIs.</p>
<p>I’m Agnes Olorundare, and you can find out more about me on <a target="_blank" href="https://www.linkedin.com/in/agnes-olorundare-446055b8/"><strong>LinkedIn</strong></a><strong><em>.</em></strong></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
