<?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" version="2.0"><channel><title><![CDATA[Kene's blog]]></title><description><![CDATA[Kene's blog]]></description><link>https://shell-script-for-monitoring-login-attempts.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 22 Jun 2026 20:37:29 GMT</lastBuildDate><atom:link href="https://shell-script-for-monitoring-login-attempts.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Built a Real-Time DDoS Detection Engine from Scratch]]></title><description><![CDATA[Introduction
Imagine you run a website that stores files for thousands of users. Everything is running smoothly until one day, your server slows to a crawl and legitimate users can't get in. You check]]></description><link>https://shell-script-for-monitoring-login-attempts.hashnode.dev/how-i-built-a-real-time-ddos-detection-engine-from-scratch</link><guid isPermaLink="true">https://shell-script-for-monitoring-login-attempts.hashnode.dev/how-i-built-a-real-time-ddos-detection-engine-from-scratch</guid><dc:creator><![CDATA[Kenechukwu Okeke]]></dc:creator><pubDate>Wed, 29 Apr 2026 22:44:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67eab4a7ec6b8ebfbb53cb5e/02c8e188-5899-4275-aa34-8b8c9453f808.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Introduction</h3>
<p>Imagine you run a website that stores files for thousands of users. Everything is running smoothly until one day, your server slows to a crawl and legitimate users can't get in. You check the logs and see one IP address sent 10,000 requests in the last minute. You've just been hit by a DDoS attack.</p>
<p>A DDoS (Distributed Denial of Service) attack is when someone floods your server with so many fake requests that it has no resources left to serve real users. It's like someone sending thousands of people to crowd a small shop so actual customers can't get in.</p>
<p>In this post I'll walk you through how I built an automated system that watches all incoming traffic, learns what normal looks like, and blocks attackers automatically — all without a human having to watch a screen.</p>
<h3>What the Project Does</h3>
<p>The system sits alongside a Nextcloud server (a self-hosted file storage app, like Google Drive) and does five things continuously:</p>
<ol>
<li><p>Reads every HTTP request as it arrives</p>
</li>
<li><p>Counts requests per IP and globally</p>
</li>
<li><p>Learns what normal traffic looks like</p>
</li>
<li><p>Flags anything that deviates from normal</p>
</li>
<li><p>Blocks attackers at the firewall level and alerts the team on Slack</p>
</li>
</ol>
<p>The entire thing runs as a background daemon — a program that never stops — and makes decisions in real time without any human involvement.</p>
<h3>The Architecture</h3>
<p>Before diving into the individual pieces, here is how everything connects:</p>
<pre><code class="language-markdown">Internet Traffic
      ↓
   Nginx (front door — logs every request as JSON)
      ↓
   Nextcloud (the actual app users interact with)
      ↓
   Your Detector (reads logs, runs math, blocks bad guys)
      ↓
   Dashboard + Slack Alerts + iptables blocks
</code></pre>
<p>Nginx sits in front of Nextcloud as a reverse proxy. Think of it like a receptionist — every visitor goes through it first. It writes down every visitor in a log file. Your detector reads that log file in real time.</p>
<h3>Piece 1 — Reading the Logs</h3>
<p>Nginx is configured to write logs in JSON format. Every single HTTP request produces one line like this:</p>
<pre><code class="language-json">{
  "source_ip": "41.58.2.1",
  "timestamp": "2024-11-14T15:04:05Z",
  "method": "GET",
  "path": "/login",
  "status": 200,
  "response_size": 4321
}
</code></pre>
<p>JSON is used instead of the default Nginx format because it is easy for Python to read. Instead of splitting strings, you just ask for a field by name:</p>
<pre><code class="language-python">data = json.loads(log_line)
ip = data["source_ip"]
status = data["status"]
</code></pre>
<p>The detector opens this file and reads it line by line forever. When there is no new line yet, it waits 50 milliseconds and tries again. This is called tailing a file — the same thing the <code>tail -f</code> command does in Linux.</p>
<h3>Piece 2 — The Sliding Window</h3>
<p>Once you have a log line, you need to answer one question: <strong>how many requests has this IP sent recently?</strong></p>
<p>"Recently" is defined as the last 60 seconds. The naive approach would be counting requests per minute — but that has a problem. If an attacker sends 1000 requests in the last 5 seconds of one minute and the first 5 seconds of the next, each minute only shows 500. The per-minute counter misses the attack completely.</p>
<p>A sliding window fixes this. It always looks at the last 60 seconds from right now, regardless of where the clock minute boundary falls.</p>
<p>The data structure used is a <strong>deque</strong> (double-ended queue). Think of it as a list where you can efficiently add to one end and remove from the other.</p>
<p>Here is how it works:</p>
<pre><code class="language-python">from collections import deque
import time

window = deque()

# When a new request arrives, add it to the right
window.append((time.time(), is_error))

# Remove entries older than 60 seconds from the left
# Entries are in time order so oldest is always on the left
now = time.time()
while window and now - window[0][0] &gt; 60:
    window.popleft()

# Current request rate = how many entries remain
rate = len(window)
</code></pre>
<p>As time moves forward, old entries fall off the left automatically. The window slides with time. At any moment, <code>len(window)</code> gives you the exact request count for the last 60 seconds.</p>
<p>Two windows are maintained simultaneously:</p>
<ul>
<li><p><strong>Per-IP window</strong> — one deque per IP address</p>
</li>
<li><p><strong>Global window</strong> — one deque for all traffic combined</p>
</li>
</ul>
<h3>Piece 3 — The Baseline</h3>
<p>Knowing the current rate is not enough. You need context. Is 15 requests per second a lot? It depends entirely on what normal looks like for your server.</p>
<p>At 3am, your server might normally get 2 requests per second. At noon it might get 30. A hardcoded threshold of "block anyone over 50 req/s" would miss the 3am attack and block legitimate noon traffic.</p>
<p>The solution is a <strong>rolling baseline</strong> — the system watches your actual traffic and learns what normal looks like.</p>
<p>Every second, the detector records how many requests arrived that second. It keeps a rolling 30-minute window of these per-second counts. Every 60 seconds it calculates two numbers from that data:</p>
<p><strong>Mean</strong> — the average requests per second:</p>
<pre><code class="language-python">mean = sum(data) / len(data)
</code></pre>
<p><strong>Standard deviation</strong> — how much the numbers normally vary from the mean:</p>
<pre><code class="language-python">variance = sum((x - mean) ** 2 for x in data) / len(data)
stddev = math.sqrt(variance)
</code></pre>
<p>These two numbers are your baseline. They update every 60 seconds so they always reflect recent reality. If your site gets a surge of legitimate traffic at noon, the baseline adjusts within a minute and stops treating that traffic as suspicious.</p>
<p>The system also keeps per-hour slots. Traffic at 9am tends to look different from traffic at 3am. When there is enough data for the current hour, it prefers that over the full 30-minute average — because the current hour is the most relevant comparison.</p>
<p>Floor values are applied to prevent extreme sensitivity during quiet periods:</p>
<pre><code class="language-python">effective_mean = max(mean, 1.0)    # never below 1 req/s
effective_stddev = max(stddev, 0.5) # never below 0.5
</code></pre>
<p>Without floors, a server with almost zero traffic at 3am would have a mean of 0.1 and a stddev of 0.05 — meaning the first legitimate morning user could trigger a ban.</p>
<h3>Piece 4 — The Detection Logic</h3>
<p>Now you have two things: the current rate for an IP, and the baseline for what normal looks like. The detection logic compares them using a <strong>Z-score</strong>.</p>
<p>A Z-score answers one question: how many standard deviations away from normal is this number?</p>
<pre><code class="language-python">zscore = (current_rate - mean) / stddev
</code></pre>
<p>If normal is 12 req/s and stddev is 2:</p>
<ul>
<li><p>14 req/s → Z-score of 1.0 → within normal range</p>
</li>
<li><p>18 req/s → Z-score of 3.0 → suspicious</p>
</li>
<li><p>50 req/s → Z-score of 19.0 → definitely an attack</p>
</li>
</ul>
<p>The system fires an alert when either of two conditions is true — whichever happens first:</p>
<pre><code class="language-python"># Condition 1 — statistically anomalous
if zscore &gt; 3.0:
    flag_as_anomaly()

# Condition 2 — raw rate too high relative to baseline
elif current_rate &gt; 5 * mean:
    flag_as_anomaly()
</code></pre>
<p>The second condition catches edge cases where the standard deviation is large (traffic varies a lot normally) and the Z-score alone might be too lenient.</p>
<p>There is also an error surge check. If an IP is getting a lot of 4xx or 5xx responses — errors — that suggests probing behavior. Someone trying every door hoping one is unlocked. When an IP's error rate exceeds 3x the baseline error rate, the detection thresholds for that IP are tightened automatically.</p>
<p>Global traffic is checked every 5 seconds using the same logic. A global spike means many IPs are attacking simultaneously — you cannot block all of them, so the system sends a Slack alert for the team to respond manually.</p>
<h3>Piece 5 — Blocking with iptables</h3>
<p>When an IP is flagged, it needs to be stopped immediately. Sending a message back saying "you're blocked" wastes server resources. The attack traffic needs to disappear before it even reaches Nginx.</p>
<p><strong>iptables</strong> is the Linux kernel's built-in firewall. It sits at the very bottom of the network stack — below Docker, below Nginx, below everything. When you add a DROP rule for an IP, that IP's packets are thrown away at the kernel level. The attacker doesn't get an error message. Their connection simply never happens.</p>
<pre><code class="language-python">import subprocess

def ban(ip):
    subprocess.run([
        "iptables", "-I", "INPUT",
        "-s", ip,
        "-j", "DROP"
    ])
</code></pre>
<p>Breaking that command down:</p>
<ul>
<li><p><code>iptables</code> — the firewall tool</p>
</li>
<li><p><code>-I INPUT</code> — insert at the TOP of the incoming traffic chain</p>
</li>
<li><p><code>-s 41.58.2.1</code> — match traffic from this source IP</p>
</li>
<li><p><code>-j DROP</code> — silently discard the packet</p>
</li>
</ul>
<p>The <code>-I</code> flag inserts at the top rather than appending to the bottom. This matters because iptables checks rules in order — if your DROP rule is at the bottom and there is an ACCEPT rule above it, traffic gets accepted before reaching your block.</p>
<p>The ban happens within 10 seconds of detection. A Slack message is sent simultaneously telling the team who was blocked, why, and how long the ban lasts.</p>
<h3>The Unban Schedule</h3>
<p>Not every block needs to be permanent. The system uses a backoff schedule:</p>
<table>
<thead>
<tr>
<th>Offense</th>
<th>Ban Duration</th>
</tr>
</thead>
<tbody><tr>
<td>1st</td>
<td>10 minutes</td>
</tr>
<tr>
<td>2nd</td>
<td>30 minutes</td>
</tr>
<tr>
<td>3rd</td>
<td>2 hours</td>
</tr>
<tr>
<td>4th+</td>
<td>Permanent</td>
</tr>
</tbody></table>
<p>A background thread checks every 10 seconds whether any ban has expired. When one has, it removes the iptables rule, updates the system state, and sends a Slack notification. If the same IP gets caught again, the next ban is longer.</p>
<p>To remove a ban:</p>
<pre><code class="language-python">subprocess.run([
    "iptables", "-D", "INPUT",
    "-s", ip,
    "-j", "DROP"
])
</code></pre>
<h3>The Live Dashboard</h3>
<p>A Flask web server runs on port 8080 and serves a page that refreshes every 3 seconds automatically using a simple HTML meta tag:</p>
<pre><code class="language-html">&lt;meta http-equiv="refresh" content="3"&gt;
</code></pre>
<p>The dashboard shows:</p>
<ul>
<li><p>Currently banned IPs with countdown timers</p>
</li>
<li><p>Global requests per second</p>
</li>
<li><p>Top 10 source IPs</p>
</li>
<li><p>CPU and memory usage</p>
</li>
<li><p>Current baseline mean and stddev</p>
</li>
<li><p>System uptime</p>
</li>
</ul>
<p>Every page load collects fresh data from all components and rebuilds the HTML. Simple and reliable.</p>
<h3>What I Learned</h3>
<p>The most important insight from this project is that <strong>detection only works when it is relative</strong>. A hardcoded threshold is always wrong for someone. Traffic patterns are unique to every server, every time of day, every day of the week. The baseline approach means the system adapts automatically — it gets smarter the longer it runs.</p>
<p>The second insight is that <strong>separation of concerns makes complex systems manageable</strong>. Each file has exactly one job. The sliding window does not know about Slack. The notifier does not know about iptables. When something breaks, you know exactly which file to look at.</p>
<hr />
<h3>Source Code</h3>
<p>The full source code is available at: <a href="https://github.com/RichardBenjamin/hng14-Stage3"><strong>https://github.com/RichardBenjamin/hng14-Stage3</strong></a></p>
<p>The live metrics dashboard is available at: <a href="http://hngstagekene3.duckdns.org:8080"><strong>http://hngstagekene3.duckdns.org:8080</strong></a></p>
<hr />
<p><em>Built as part of the HNG DevSecOps track — Stage 3.</em></p>
]]></content:encoded></item><item><title><![CDATA[Building a Lightweight IaC Misconfiguration Detector in Go]]></title><description><![CDATA[Introduction
Imagine pushing a Terraform file with a wildcard IAM role to production without realizing it. Oops. Now imagine getting a Slack notification seconds after the CI pipeline spots that misconfiguration. Much better, right?
This project will...]]></description><link>https://shell-script-for-monitoring-login-attempts.hashnode.dev/building-a-lightweight-iac-misconfiguration-detector-in-go</link><guid isPermaLink="true">https://shell-script-for-monitoring-login-attempts.hashnode.dev/building-a-lightweight-iac-misconfiguration-detector-in-go</guid><category><![CDATA[AWS]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Docker]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Regex]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Kenechukwu Okeke]]></dc:creator><pubDate>Sat, 12 Apr 2025 11:00:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744198303821/bc031dfc-440b-47a9-97d1-b8de5c449aa0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2 id="heading-introduction">Introduction</h2>
<p>Imagine pushing a Terraform file with a wildcard IAM role to production without realizing it. Oops. Now imagine getting a Slack notification seconds after the CI pipeline spots that misconfiguration. Much better, right?</p>
<p>This project will walk through the building and deployment of a lightweight Infrastructure-as-Code (IaC) misconfiguration scanner built in Go. This scanner integrates seamlessly into GitHub Actions and sends scan reports both as GitHub artifacts and to AWS S3—with Slack notifications to keep the team alert. The project guide is divided into four sections:</p>
<ul>
<li><p>Scanner Architecture and Logic</p>
</li>
<li><p>GitHub Actions Pipeline Breakdown</p>
</li>
<li><p>Creating AWS IAM User and S3 Bucket for CI Pipeline</p>
</li>
<li><p>Slack Integration Setup</p>
</li>
</ul>
<p>And the end of this guide, you'll have:</p>
<ul>
<li><p>A working IaC security scanner</p>
</li>
<li><p>CI/CD automation with GitHub Actions</p>
</li>
<li><p>Cloud storage of scan results in S3</p>
</li>
<li><p>Real-time Slack alerts with links to the scan logs</p>
</li>
</ul>
<p>Link to the project: <a target="_blank" href="https://github.com/RichardBenjamin/IAC-Scanner">https://github.com/RichardBenjamin/IAC-Scanner</a></p>
<hr />
<h2 id="heading-project-overview-and-scanner-architecture">Project Overview and Scanner Architecture</h2>
<h3 id="heading-project-structure">Project Structure</h3>
<pre><code class="lang-bash">IAC-Scanner/
├── main.go                  <span class="hljs-comment"># Entry point</span>
├── scanner/
│   ├── scanner.go          <span class="hljs-comment"># Main scanning logic</span>
│   ├── docker.go           <span class="hljs-comment"># Handles Dockerfile scans</span>
│   ├── k8s.go              <span class="hljs-comment"># Handles Kubernetes YAML scans</span>
│   └── terraform.go        <span class="hljs-comment"># Handles Terraform file scans</span>
├── rules/
│   ├── rule.go             <span class="hljs-comment"># Common rule structure</span>
│   ├── docker_rules.go     <span class="hljs-comment"># Rule definitions for Docker</span>
│   ├── k8s_rules.go        <span class="hljs-comment"># Rule definitions for Kubernetes</span>
│   └── tf_rules.go         <span class="hljs-comment"># Rule definitions for Terraform</span>
├── test-files/             <span class="hljs-comment"># Sample misconfigured files for testing</span>
</code></pre>
<h2 id="heading-scanner-architecture-and-logic">Scanner Architecture and Logic</h2>
<p>The scanner project is written in Go and is structured in a modular way for clarity and ease of maintenance. It begins execution from the <code>main.go</code> file, which is the starting point of the program. This file accepts a command-line argument that specifies the folder or file path to scan. It then passes this path to the <code>RunScanner</code> function in <code>scanner.go</code>.</p>
<h3 id="heading-maingo">main.go</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(os.Args) &lt; <span class="hljs-number">2</span> {
        fmt.Println(<span class="hljs-string">"Usage: iac-scan &lt;path-to-scan&gt;"</span>)
        os.Exit(<span class="hljs-number">1</span>)
    }
    path := os.Args[<span class="hljs-number">1</span>]
    scanner.RunScanner(path)
}
</code></pre>
<p>Inside <code>scanner.go</code>, the <code>RunScanner</code> function creates a file named <code>scan-results.log</code>. This file will be used to store all scan results. The <code>defer file.Close()</code> line ensures that once scanning is done, the file is properly closed—even if an error occurs later. This setup provides a clean way to collect and save output from the scanner</p>
<p>As it encounters each file, it checks the file name and extension to determine its type. For example:</p>
<ul>
<li><p>Files ending in <code>.tf</code> are identified as Terraform files</p>
</li>
<li><p>Files ending in <code>.yaml</code> or <code>.yml</code> are assumed to be Kubernetes YAML files</p>
</li>
<li><p>Any file named <code>Dockerfile</code> or starting with "docker" is treated as a Dockerfile</p>
</li>
</ul>
<p>Once the type is known, the corresponding handler function is called — <code>CheckTerraform</code>, <code>CheckKubernetesYAML</code>, or <code>CheckDockerfile</code>. These functions are defined in their respective files: <code>terraform.go</code>, <code>k8s.go</code>, and <code>docker.go</code>.</p>
<p>Each handler reads the content of the file and compares it with a list of predefined rules. These rules are written using regular expressions (regex), which are patterns used to search text. The rules are defined in separate files under the <code>/rules/</code> folder. Each rule includes:</p>
<ul>
<li><p>A <code>Name</code> to identify it</p>
</li>
<li><p>A <code>Category</code> to group it (like permissions, secrets, etc.)</p>
</li>
<li><p>A <code>Severity</code> level (LOW, MEDIUM, or HIGH)</p>
</li>
<li><p>A <code>Pattern</code> which is a regex used to match against the file's contents</p>
</li>
<li><p>A <code>Message</code> to explain what was found</p>
</li>
<li><p>An <code>Enabled</code> flag to turn the rule on or off</p>
</li>
</ul>
<h3 id="heading-rule-struct-in-rulego"><code>Rule Struct (in rule.go)</code></h3>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Rule <span class="hljs-keyword">struct</span> {
    Name     <span class="hljs-keyword">string</span>
    Category <span class="hljs-keyword">string</span>
    Severity <span class="hljs-keyword">string</span>
    Pattern  <span class="hljs-keyword">string</span>
    Message  <span class="hljs-keyword">string</span>
    Enabled  <span class="hljs-keyword">bool</span>
}
</code></pre>
<p>Rules are defined as structs and stored in slices, categorized by file type. A struct (short for structure) is a composite data type that groups together zero or more fields (variables) under a single name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744188575295/bdab2af9-c560-42e1-87dd-80ed4ce96bab.png" alt class="image--center mx-auto" /></p>
<p><em>Sample of rules set in Kubernetes_rules.go</em></p>
<p>If a match is found between the rule and the file content, a function called <code>ReportIssue</code> is triggered. This function prints the issue to the terminal and also writes it into a file named <code>scan-results.log</code>. This makes it easy to view the output later or use it in CI/CD pipelines.</p>
<p>This approach makes the scanner easy to extend and organize allowing it to work well with automation tools like GitHub Actions. It also makes it fast and independent on external tools.</p>
<hr />
<h2 id="heading-github-actions-pipeline-breakdown">GitHub Actions Pipeline Breakdown</h2>
<p>The GitHub Actions pipeline automates the entire scanning process every time someone pushes code or opens a pull request to the <code>main</code> branch. The breakdown of the pipeline is below.</p>
<h3 id="heading-checkout-code"><code>Checkout Code</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">Code</span>
  <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>
</code></pre>
<p>This step tells GitHub Actions to "check out" the repository code, meaning it pulls (clones) the contents of the repo into the runner. The runner is a temporary virtual machine that runs the workflow. Without this step, the runner would be empty and wouldn't know anything about the code!</p>
<h3 id="heading-set-up-go"><code>Set up Go</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Go</span>
  <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-go@v4</span>
  <span class="hljs-attr">with:</span>
    <span class="hljs-attr">go-version:</span> <span class="hljs-number">1.22</span>
</code></pre>
<p>This step <strong>installs the Go programming language</strong> in the workflow runner. The scanner is written in Go, so the runner needs Go installed to compile it. The version is kept at <strong>1.22</strong> to make sure the behavior is consistent with what the scanner was built and tested with. That avoids weird bugs that can happen when using a different version.</p>
<h3 id="heading-build-scanner"><code>Build Scanner</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Scanner</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">go</span> <span class="hljs-string">build</span> <span class="hljs-string">-o</span> <span class="hljs-string">iac-scan</span>
</code></pre>
<p>This command uses <code>go build</code> to compile the source code in the repo and outputs an executable file named <code>iac-scan</code>. The <code>-o</code> flag stands for "output". It tells the <code>go build</code> command what to name the executable file. Without <code>-o</code>, Go would just name the file after the directory by default.</p>
<h3 id="heading-run-scanner"><code>Run Scanner</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Scanner</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">./iac-scan</span> <span class="hljs-string">./test-files</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">scan-results.log</span>
</code></pre>
<p>The command <code>./iac-scan</code> runs the executable file named <code>iac-scan</code>, which was compiled in the previous step. It takes <code>./test-files</code> as the input directory, instructing the scanner to analyze the files contained within it. The <code>&gt; scan-results.log</code> part redirects the scanner's output (stdout) into a file named <code>scan-results.log</code>, effectively saving the scan results instead of displaying them in the console.</p>
<h3 id="heading-upload-artifact"><code>Upload Artifact</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">Artifact</span>
  <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v2</span>
  <span class="hljs-attr">with:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">iac-scan-report</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">scan-results.log</span>
</code></pre>
<p>This line itells GitHub Actions to <strong>use a prebuilt action called</strong> <code>upload-artifact</code>, which is maintained by the official <code>actions</code> team at GitHub. The <code>@v2</code> part specifies that the code uses <strong>version 2</strong> of that action. The <code>actions/upload-artifact@v2</code> action saves the <code>scan-results.log</code> file as a downloadable artifact named <code>iac-scan-report</code>.</p>
<p><strong>Artifacts</strong> are files that GitHub Actions workflow <strong>uploads and saves after the run is complete</strong>. After the workflow finishes, find and download this artifact by going to the <strong>Actions tab</strong> of the GitHub repository and selecting the relevant workflow run.</p>
<h3 id="heading-fail-if-high-severity-issues-are-found"><code>Fail if HIGH Severity Issues Are Found</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Fail</span> <span class="hljs-string">if</span> <span class="hljs-string">HIGH</span> <span class="hljs-string">severity</span> <span class="hljs-string">issues</span> <span class="hljs-string">are</span> <span class="hljs-string">found</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">scan_check</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">|
    if grep -q "\[HIGH\]" scan-results.log; then
      message="High severity issues detected in files!"
      echo "message=$message" &gt;&gt; $GITHUB_OUTPUT
      exit 1
    else
      message="No HIGH severity issues found."
      echo "$message"
      echo "message=$message" &gt;&gt; $GITHUB_OUTPUT
    fi</span>
</code></pre>
<p>The "Fail if HIGH severity issues are found" step in a GitHub Actions workflow checks the <code>scan-results.log</code> file for any lines marked with <code>[HIGH]</code> to identify critical security issues. Using a <code>grep</code> command, it determines whether such issues exist and sets a corresponding <code>message</code> output. If high- severity issues are detected, it records a failure message and exits with code <code>1</code>, intentionally failing the job to enforce security policies. If no such issues are found, it logs a success message and allows the workflow to continue.</p>
<h3 id="heading-configure-aws-credentials"><code>Configure AWS Credentials</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Configure</span> <span class="hljs-string">AWS</span> <span class="hljs-string">credentials</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">always()</span>
  <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/configure-aws-credentials@v2</span>
  <span class="hljs-attr">with:</span>
    <span class="hljs-attr">aws-access-key-id:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCESS_KEY_ID</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">aws-secret-access-key:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_ACCESS_KEY</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">aws-region:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_REGION</span> <span class="hljs-string">}}</span>
</code></pre>
<p>This step sets up the GitHub Actions environment with the necessary AWS credentials to interact with AWS services like S3. The <code>aws-actions/configure-aws-credentials</code> action configures the AWS CLI with the secret keys stored in GitHub Secrets. The <code>if: always()</code> condition ensures this step runs regardless of whether earlier steps fail or succeed—making it useful when uploading logs.</p>
<p>The <code>--acl public-read</code> flag is included to make the uploaded file publicly accessible via a direct URL. This is useful for sharing the scan results externally without requiring authentication. The AWS credentials (<code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code>) are securely stored as GitHub Secrets and inserted into the workflow environment, so sensitive information are never exposed in the code.</p>
<h3 id="heading-set-log-filename"><code>Set log filename</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">log</span> <span class="hljs-string">filename</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">always()</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">logfile</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">"filename=logs/log-$<span class="hljs-template-variable">{{ github.run_id }}</span>.txt"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">$GITHUB_OUTPUT</span>
</code></pre>
<p>This step creates a dynamic filename for the scan log file to be stored in the S3 bucket. By assigning a unique name using the <code>github.run_id</code>, each workflow run generates a distinct log file. The value is saved to an output variable called <code>filename</code>, which can be referenced later using <code>steps.logfile.outputs.filename</code>.</p>
<h3 id="heading-upload-to-s3"><code>Upload to S3</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">to</span> <span class="hljs-string">S3</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">aws</span> <span class="hljs-string">s3</span> <span class="hljs-string">cp</span> <span class="hljs-string">scan-results.log</span> <span class="hljs-string">s3://$AWS_S3_BUCKET/scan-results.log</span> <span class="hljs-string">--acl</span> <span class="hljs-string">public-read</span>
</code></pre>
<p>The <code>Upload to S3</code> step uses the AWS CLI command <code>aws s3 cp</code> to copy the <code>scan-results.log</code> file into an S3 bucket. The destination is specified as <code>s3://$AWS_S3_BUCKET/scan-results.log</code>, where <code>$AWS_S3_BUCKET</code> is an environment variable pointing to the name of the S3 bucket.</p>
<h3 id="heading-generate-pre-signed-s3-url"><code>Generate Pre-signed S3 URL</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Get</span> <span class="hljs-string">S3</span> <span class="hljs-string">Pre-signed</span> <span class="hljs-string">URL</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">always()</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">signed_url</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">|
    url=$(aws s3 presign s3://my-ci-logs-bucket/${{ steps.logfile.outputs.filename }} --expires-in 3600)
    echo "url=$url" &gt;&gt; $GITHUB_OUTPUT</span>
</code></pre>
<p>This step creates a temporary, pre-signed URL that can be shared with others to access the scan log file in S3 without requiring AWS credentials. The link is valid for one hour (<code>3600</code> seconds). The generated URL is stored in the output variable <code>url</code> for use in Slack notifications or other steps.</p>
<h3 id="heading-notify-slack"><code>Notify Slack</code></h3>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Notify</span> <span class="hljs-string">Slack</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">|
      curl -X POST -H 'Content-type: application/json' \
          --data "{
             \"text\": \"*Scan Complete*\\n${{ steps.scan_check.outputs.message }}\\n
           *S3 Logs:* &lt;${{ steps.signed_url.outputs.url }}&gt;
          *GitHub Artifact:* &lt;https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}&gt;
              \"}" \
              ${{ secrets.SLACK_WEBHOOK_URL2}}</span>
</code></pre>
<p>The <code>Notify Slack</code> step uses <code>curl</code> to send a POST request to a <strong>Slack Incoming Webhook URL</strong>, which is stored securely as a GitHub Secret (<code>SLACK_WEBHOOK_URL</code>). An <strong>Incoming Webhook URL</strong> is a special Slack-generated URL that allows external systems, like GitHub Actions, to send messages directly into a Slack channel.</p>
<p>The message sent is a simple JSON payload that posts a notification saying <em>" Scan complete! S3 Logs: [link]"</em> — with the link pointing to the publicly accessible scan report on S3 and adds a direct link to the GitHub Actions run that produced this scan. This allows the team to get immediate alerts in a Slack channel whenever a scan finishes, making it easy to respond quickly to issues without having to dig into logs manually.</p>
<p>By joining all these steps together, a pipeline that scans every commit or pull request for infrastructure misconfigurations and immediately alerts the team with a downloadable report.</p>
<hr />
<h2 id="heading-creating-aws-iam-user-and-s3-bucket-for-ci-pipeline">Creating AWS IAM User and S3 Bucket for CI Pipeline</h2>
<p>To connect GitHub Actions workflow to AWS, a AWS IAM user that has access to a specific S3 bucket where the scan reports will be stored is needed.</p>
<h3 id="heading-step-1-create-iam-user-for-ci">Step 1: Create IAM User for CI</h3>
<p>Go to the AWS IAM Dashboard and click <strong>Users</strong> &gt; <strong>Add user</strong>. Name the user something like <code>github-ci-user</code>. Under <strong>Access Type</strong>, check:</p>
<ul>
<li><p>Programmatic access (for access key and secret)</p>
</li>
<li><p>AWS Management Console access to manually log in later</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744188906990/3dcaee6e-f677-4873-9fdc-9cf149151073.png" alt /></p>
<p>  <em>Image showing the creation of IAM User</em></p>
</li>
</ul>
<h3 id="heading-step-2-set-user-permissions">Step 2: Set User Permissions</h3>
<p>On the next page, choose <strong>Attach policies directly and pick</strong> <code>AmazonS3FullAccess</code>. This grants full access to all Amazon S3 resources within an AWS account. When this policy is attached to an IAM user, group, or role, it allows that entity to perform <strong>any action (</strong><code>s3:*</code>) on <strong>any bucket or object (</strong><code>*</code>) in Amazon S3.</p>
<ul>
<li><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744189209896/a24b69f0-05a2-4da0-a94d-fa873cbad9a3.png" alt class="image--center mx-auto" /></li>
</ul>
<p><em>Image showing policy attached to user</em></p>
<h3 id="heading-step-3-generate-access-keys">Step 3: Generate Access Keys</h3>
<p>After the user is created, go to <strong>Security credentials</strong> tab &gt; <strong>Create access key</strong>. Choose "Application running outside AWS". This tells AWS that the access key is intended for use in an environment that is not within the AWS like Github Actions. Copy the Access Key ID and Secret Access Key and store them because the secrets won’t be able to be viewed again, and leaking it can allow full programmatic access to the AWS resources depending on the permissions tied to the user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744189810135/8ca56da7-b879-4429-8456-fd96f31d06fb.png" alt class="image--center mx-auto" /></p>
<p><em>Image showing the generation of Access Keys</em></p>
<h3 id="heading-step-4-add-access-to-github-secrets">Step 4: Add Access to GitHub Secrets</h3>
<p>Go to the GitHub repository &gt; Settings &gt; Secrets and variables &gt; Actions. Add:</p>
<ul>
<li><p><code>AWS_ACCESS_KEY_ID</code></p>
</li>
<li><p><code>AWS_SECRET_ACCESS_KEY</code></p>
</li>
<li><p><code>AWS_S3_BUCKET</code></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744190625411/e3732692-2a13-4df5-b8ab-b280a0388e8b.png" alt /></p>
<p>  <em>Repository secrets in GitHub Actions workflow.</em></p>
</li>
</ul>
<h3 id="heading-step-5-create-s3-bucket">Step 5: Create S3 Bucket</h3>
<p>Now go to the <strong>S3 service</strong> in AWS and click <strong>Create bucket</strong>. Enter a unique name (e.g., <code>iac-ci-logs</code>) and select a region. Leave other settings default and click Create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744190731533/a0e77889-47b3-450a-aced-553f80786c89.png" alt class="image--center mx-auto" /></p>
<p><em>Image showing the creation of S3 bucket</em></p>
<h3 id="heading-step-6-make-bucket-object-public">Step 6: Make Bucket Object Public</h3>
<p>To directly the Slack notification to the report, allow public read access to the file. To do this add an ACL or edit the bucket policy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744190845872/6b093646-8b28-45b5-b56f-fcac448d6cb7.png" alt class="image--center mx-auto" /></p>
<p><em>Image showing the creation of S3 Bucket policy</em></p>
<h2 id="heading-slack-integration-setup">Slack Integration Setup</h2>
<p>To ensure that the team gets immediate feedback when IaC misconfigurations are found, slack is integrated into the CI process. This involves creating a Slack App that can send messages to the desired Slack channel using incoming webhooks and OAuth tokens.</p>
<h3 id="heading-step-1-create-a-slack-app">Step 1: Create a Slack App</h3>
<p>Go to Slack API and click <strong>Create New App</strong>. Choose <strong>From scratch</strong>. Then name the app (e.g., <code>IAC-Scanner-Alert</code>) and select the appropriate Slack workspace.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744191132453/273c96f3-fefb-4daf-b2db-fd1a0a36435e.png" alt class="image--center mx-auto" /></p>
<p><em>Image showing the creation of slack app</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744191160259/a069f72d-54ec-4ed0-ac06-b7a43d81c266.png" alt /></p>
<p><em>Image showing the selection of App name and workspace</em></p>
<h3 id="heading-step-2-add-scopes-and-permissions">Step 2: Add Scopes and Permissions</h3>
<p>Inside the app settings, go to <strong>OAuth &amp; Permissions</strong>. Under <strong>Bot Token Scopes</strong>, add the following:</p>
<ul>
<li><p><code>incoming-webhook</code> – To allow posting to channels</p>
</li>
<li><p><code>chat:write</code> – To enable the app to send messages</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744191230846/3fbc62c6-904b-423f-935e-f3fb4faaa19d.png" alt /></p>
</li>
</ul>
<p><em>Image showing OAuth Scopes section showing selected scopes</em></p>
<h3 id="heading-step-3-install-app-to-workspace">Step 3: Install App to Workspace</h3>
<p>Scroll to the top and click <strong>Install App to Workspace</strong>. You'll be redirected to grant permissions. Choose the channel the app can post to (e.g., <code>#all-devsecops</code>) and authorize.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744191269665/e029f669-c478-48a7-898c-dd4a0129c7fb.png" alt /></p>
<p><em>Image showing permission request for a selected channel</em></p>
<h3 id="heading-step-4-copy-credentials">Step 4: Copy Credentials</h3>
<p>After installation, this will be provided:</p>
<ul>
<li><p>A <strong>Bot User OAuth Token</strong> (starts with <code>xoxb-...</code>)</p>
</li>
<li><p>A <strong>Webhook URL</strong> for the selected channel</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744191333045/e2fb3b60-47ef-4bc8-86f7-e98995448e6b.png" alt /></p>
</li>
</ul>
<p><em>Image showing the Bot User OAuth Token to be copied</em></p>
<p>Copy these and add them to the GitHub repository’s secrets as:</p>
<ul>
<li><p><code>SLACK_WEBHOOK_URL</code></p>
</li>
<li><p><code>SLACK_BOT_TOKEN</code></p>
</li>
</ul>
<h3 id="heading-step-5-test-the-application">Step 5: Test the Application</h3>
<p>After the Slack has been set up and the channel has invited the app. Test the scanner alert by running the scanner with the sample misconfigured IaC files in the testing folder. The following alert would look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744192210334/1c74556f-9401-4d22-a1a0-7b0dd9d4fd8d.png" alt class="image--center mx-auto" /></p>
<p><em>Image showing the links to the log send to the slack channel through the app.</em></p>
<h2 id="heading-errors-encountered-and-how-i-handled-them">Errors Encountered and How I Handled Them</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Error Encountered</strong></td><td><strong>How it was Handled</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Scan results not uploading to S3</td><td>Verified IAM permissions and bucket policy; ensured filename path was correct</td></tr>
<tr>
<td>GitHub Secrets not picked up in CI</td><td>Checked secrets configuration under repository settings and ensured correct naming</td></tr>
<tr>
<td>Pre-signed URL not working</td><td>Adjusted the expiration time and ensured correct file path was used with <code>aws s3 presign</code></td></tr>
<tr>
<td>Slack alert not delivering</td><td>Fixed webhook URL and updated bot token permissions in the Slack app dashboard</td></tr>
<tr>
<td>AWS S3 upload failed</td><td>Double-checked bucket name, region, and IAM user permissions; regenerated access keys if needed</td></tr>
<tr>
<td>Scan results not appearing in GitHub artifacts</td><td>Fixed file path and ensured log file was created before artifact upload step ran</td></tr>
<tr>
<td>False positive in rule match</td><td>Refined the regex pattern and tested against both vulnerable and safe sample files</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>This project builds a Go-based IaC scanner that detects misconfigurations in Terraform, Kubernetes, and Docker files using regex rules. It automates its execution in a GitHub Actions pipeline, where results are saved as artifacts, pushed to AWS S3, and shared via Slack alerts. It’s a lightweight, approach to add security to CI/CD workflows and helps the team catch issues early. If you enjoyed this, feel free to share, comment, and connect!</p>
]]></content:encoded></item><item><title><![CDATA[Shell Script for Monitoring Login Failures and Reporting Unauthorized Access Attempts]]></title><description><![CDATA[Introduction
Security is an important part of system administration, and one of the most common attack vectors on a Linux system is unauthorized login attempts. These often come in the form of brute-force attacks on SSH or other authentication method...]]></description><link>https://shell-script-for-monitoring-login-attempts.hashnode.dev/shell-script-for-monitoring-login-failures-and-reporting-unauthorized-access-attempts</link><guid isPermaLink="true">https://shell-script-for-monitoring-login-attempts.hashnode.dev/shell-script-for-monitoring-login-failures-and-reporting-unauthorized-access-attempts</guid><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Devops]]></category><category><![CDATA[automation]]></category><category><![CDATA[Script]]></category><category><![CDATA[Linux]]></category><category><![CDATA[bash script]]></category><dc:creator><![CDATA[Kenechukwu Okeke]]></dc:creator><pubDate>Thu, 03 Apr 2025 09:51:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743673981677/684406d5-e178-4074-a9ae-49062457eef5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Security is an important part of system administration, and one of the most common attack vectors on a Linux system is unauthorized login attempts. These often come in the form of brute-force attacks on SSH or other authentication methods. In this article, I will walk you through how to create a Bash script that not only monitors login failures in real time but also captures the attackers's identity using a webcam and emails a detailed alert to the system owner.</p>
<hr />
<h2 id="heading-why-bash">Why Bash?</h2>
<p><strong>Bash (Bourne Again SHell)</strong> is a widely-used command-line interpreter that is standard in most Unix-like systems. It's powerful for automation tasks due to its:</p>
<ul>
<li><p><strong>Ease of use</strong>: It has a simple syntax, readable logic.</p>
</li>
<li><p><strong>Universality</strong>: It is present on almost every Linux system.</p>
</li>
<li><p><strong>Efficiency</strong>: It is excellent for file monitoring, log analysis, process automation, etc.</p>
</li>
<li><p><strong>No need for external dependencies</strong>: It uses built-in tools like <code>grep</code>, <code>tail</code>, <code>ping</code>, etc.</p>
</li>
</ul>
<p>When paired with other Unix utilities, Bash becomes a powerful tool for building lightweight security scripts.</p>
<hr />
<h2 id="heading-what-does-the-script-do">What does the Script do?</h2>
<p>The script achieves the following key functions:</p>
<ol>
<li><p>Real time monitoring of login attempts from the device authentication log.</p>
</li>
<li><p>Captures Identity of user attempting login after three consecutive failed login attempts.</p>
</li>
<li><p>Stays active probing for internet connectivity to send an Email to the Owner in cases where the device is not connected to the internet when the attack happens.</p>
</li>
<li><p>Alerts the Owner’s Email Address the Time and captured Image of user currently attempting login.</p>
</li>
<li><p>Schedules the script to run automatically when the user initiates a login, using a cron job.</p>
</li>
</ol>
<p>Access the script via: <a target="_blank" href="https://github.com/RichardBenjamin/login-monitor-alert/">https://github.com/RichardBenjamin/login-monitor-alert/</a></p>
<hr />
<h2 id="heading-linux-utilities-used">Linux Utilities Used</h2>
<p>A brief description of the key tools and commands used in the script are outlined below:</p>
<ul>
<li><p><code>tail -F</code>: To follow and monitor the Authentication log file; <code>/var/log/auth.log</code> in real time.</p>
</li>
<li><p><code>tee</code>: To write log lines to another file for analysis and auditing.</p>
</li>
<li><p><code>grep</code>: To search for specific keywords like “<code>authentication failure</code>".</p>
</li>
<li><p><code>fswebcam</code>: Lightweight command-line tool to capture webcam images.</p>
</li>
<li><p><code>ping</code>: Used to check if internet connectivity is available.</p>
</li>
<li><p><code>msmtp</code>: A simple SMTP client to send emails through external SMTP servers.</p>
</li>
<li><p><code>uuencode</code>: Encodes binary files (images) so they can be attached via email.</p>
</li>
<li><p><code>truncate</code>: Clears log file contents before use to avoid processing old data.</p>
</li>
<li><p><code>cron</code>: A time-based job scheduler used to automate the script execution.</p>
</li>
<li><p><code>rsyslog</code>: A high-performance logging service used in Linux systems to collect, process, and store log messages.</p>
</li>
<li><p><code>TLS</code>: is a cryptographic protocol designed to provide secure communication over a network, especially for web browsing, email, and other internet services.</p>
</li>
</ul>
<p>Tools used in the script, I installed using the command below on my Linux Distro:</p>
<pre><code class="lang-bash">sudo apt update &amp;&amp; sudo apt install fswebcam msmtp sharutils
</code></pre>
<hr />
<h2 id="heading-step-by-step-process-of-script-implementation-and-execution">Step-by-step process of script implementation and execution</h2>
<h3 id="heading-step-1-create-the-script-file">Step 1: Create the Script File</h3>
<pre><code class="lang-bash">touch login-monitor.sh
chmod +x login-monitor.sh
</code></pre>
<p>Create a .sh file with a name of your choosing. For my script, I used the <code>touch</code> command to create the file then named it <code>login-monitor.sh</code>. Make the file executable with the <code>chmod +x</code> command.The “chmod +x” command is a basic Unix permission command that makes files or binaries executable. So, if the file is not executable, the shell will not allow direct execution in a format like <code>./login-monitor.sh</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743442540900/aacd949b-362e-431c-b67b-d2334ef6ace7.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-step-2-initialize-and-prepare-log-paths">Step 2: Initialize and Prepare Log Paths</h3>
<pre><code class="lang-bash">LOGFILE=<span class="hljs-string">"/var/log/auth.log"</span>
COPY_LOG=<span class="hljs-string">"/home/kene/Documents/Project/Scripts/auth_copy.log"</span>
THRESHOLD=3
FAIL_COUNT=0

truncate -s 0 <span class="hljs-string">"<span class="hljs-variable">$COPY_LOG</span>"</span>
</code></pre>
<ul>
<li><p>Open the script with an editor (Vim, Nano, etc) of your choosing.</p>
</li>
<li><p>Initialize and prepare log paths for the script.</p>
</li>
<li><p>The <code>auth_copy.log</code> in the path assigned to the variable <code>COPY_LOG</code> stores copied logs for the current session in real time from <code>/var/log/auth.log</code> .</p>
</li>
<li><p>To prevent disrupting auditing processes, the <code>auth_copy.log</code> file is used for auditing and analysis instead of the <code>/var/log/auth.log</code> .</p>
</li>
<li><p>The <code>/var/log/auth.log</code> is managed by <code>rsyslog</code> thus tampering it may cause logging issues and violate compliance standards. Using the <code>auth_copy.log</code> file completely removes sudo permission-request errors.</p>
</li>
<li><p><code>truncate -s 0</code> clears any previous logs in the <code>auth_copy.log</code> file. This allows new logs for a fresh session be analysed without confusion.</p>
</li>
</ul>
<hr />
<h3 id="heading-step-3-monitor-logs-in-real-time-and-mirror-to-backup">Step 3: Monitor Logs in Real-Time and Mirror to Backup</h3>
<pre><code class="lang-bash">tail -F <span class="hljs-string">"<span class="hljs-variable">$LOGFILE</span>"</span> | tee -a <span class="hljs-string">"<span class="hljs-variable">$COPY_LOG</span>"</span> | <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r line; <span class="hljs-keyword">do</span>
</code></pre>
<ul>
<li><ul>
<li><p><code>tail -F "$LOGFILE"</code>: This command continuously monitors <code>/var/log/auth.log</code> in real-time.</p>
<ul>
<li><p><code>tee -a "$COPY_LOG"</code>: The <code>tee</code> command after reading each new line from the <code>/var/log/auth.log</code> appends a copy to <code>auth_copy.log</code> using the <code>-a</code> option. This means the command will add new logs to the <code>auth_copy.log</code> without modifying its existing contents.</p>
</li>
<li><p><code>while read -r line; do</code>: This line applies <a class="post-section-overview" href="#step-4-handle-successful-login-events">this</a> conditional logic on each log line appended by the <code>tee</code> command.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr />
<h3 id="heading-step-4-handle-successful-login-events">Step 4: Handle Successful Login Events</h3>
<pre><code class="lang-bash"><span class="hljs-keyword">if</span> <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$line</span>"</span> | grep -q <span class="hljs-string">"session opened for user"</span>; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Successful login detected: <span class="hljs-variable">$line</span>"</span>
    FAIL_COUNT=0
    handle_success
    <span class="hljs-built_in">exit</span> 0  <span class="hljs-comment">#  To ensure complete termination</span>
<span class="hljs-keyword">fi</span>
</code></pre>
<h3 id="heading-breakdown-of-if-condition-successful-login-attempts">Breakdown of IF condition successful Login Attempts:</h3>
<ul>
<li><p><code>grep -q "session opened for user"</code>: This filters lines from the log that indicate a successful user login. It basically searches for the phrase <code>session opened for user</code> inside the <code>auth_copy.log</code> file. The <code>-q</code> option which means quiet mode hides the detail of the <code>grep -q "session opened for user"</code> command from displaying on the terminal.</p>
</li>
<li><p><code>echo "Successful login detected: $line"</code>: This receives input from <code>grep -q "session opened for user"</code> command, parses the input to the <code>$line</code> then indicates who successfully logged in and when via the terminal.</p>
</li>
<li><p><code>FAIL_COUNT=0</code>: This resets the failed login counter to avoid false positives. If the legitimate user logs in successfully, the alert logic is halted.</p>
</li>
<li><p><code>handle_success</code>: <a class="post-section-overview" href="#step-5-the-handle_success-function">This</a> function handles termination of the script. It exits the script and intercept signals if needed.</p>
</li>
<li><p><code>exit 0</code>: Ensures immediate exit from the script's current execution context after a valid login is confirmed. <code>exit 0</code> alone doesn’t stop all background processes, it's added to complement the <code>handle_success</code> function in termination of the script. Get detailed explanation of the <code>handle_success</code> function <a class="post-section-overview" href="#step-5-the-handle_success-function">here</a>.</p>
</li>
</ul>
<h3 id="heading-step-5-the-handlesuccess-function">Step 5: The <code>handle_success()</code> Function</h3>
<p>The helper function is defined to handle a clean script exit when a successful login is detected:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">handle_success</span></span>() {
    <span class="hljs-built_in">kill</span> 0  <span class="hljs-comment"># Terminates all processes in the current script group</span>
    <span class="hljs-built_in">exit</span> 0  <span class="hljs-comment"># Ensures script stops immediately</span>
}
</code></pre>
<p>In a normal Bash script, <code>exit 0</code> is used to stop the script, but in this case, it's not enough. The reason is because this script relies on a pipeline that includes <code>tail -F</code>, <code>tee</code>, and <code>while read</code>, which creates a subshell.</p>
<p>Calling <code>exit 0</code> within this loop only exits the subshell — it does not terminate the <code>tail</code> and <code>tee</code> processes that are still running. As a result, the script continues running in the background and does not stop after the login has been successful.</p>
<p>To solve this, use this command inside the script in form of a function:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">handle_success</span></span>() {
    <span class="hljs-built_in">kill</span> 0  <span class="hljs-comment"># Terminates all processes in the current script group</span>
    <span class="hljs-built_in">exit</span> 0  <span class="hljs-comment"># Ensures script stops immediately</span>
}
</code></pre>
<p>This sets up signal handling that allows the script to intercept termination signals and shut down all processes after the login has been successful.</p>
<hr />
<h3 id="heading-step-6-detecting-failed-login-attempts">Step 6: Detecting Failed Login Attempts</h3>
<p>This section deals with the intrusion detection system (IDS). It looks for failed authentication entries in the log and tracks the number of consecutive failures:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">if</span> <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$line</span>"</span> | grep -q <span class="hljs-string">"authentication failure"</span>; <span class="hljs-keyword">then</span>
    ((FAIL_COUNT++))
    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$FAIL_COUNT</span>"</span> -ge <span class="hljs-string">"<span class="hljs-variable">$THRESHOLD</span>"</span> ]; <span class="hljs-keyword">then</span>
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"ALERT: <span class="hljs-variable">$THRESHOLD</span> consecutive login failures detected!"</span>
</code></pre>
<h3 id="heading-breakdown-of-if-condition-failed-login-attempts">Breakdown of IF condition failed Login Attempts:</h3>
<ul>
<li><p><code>grep -q "authentication failure"</code>: This searches the log line for the keyword indicating a failed login attempt.</p>
</li>
<li><p><code>((FAIL_COUNT++))</code>: This increments the failure counter using Bash’s arithmetic evaluation to keep track of how many consecutive failures have occurred.</p>
</li>
<li><p><code>if [ "$FAIL_COUNT" -ge "$THRESHOLD" ]</code>: Once the failure count reaches or exceeds the predefined 3 attempts in threshold, it triggers the next part of the script, which is an email alert.</p>
</li>
<li><p><code>echo "ALERT:..."</code>: This provides visual feedback of output on the terminal.</p>
</li>
</ul>
<hr />
<h3 id="heading-step-7-capture-attackers-snapshot">Step 7: Capture Attacker’s Snapshot</h3>
<p>After the script detects the defined number of failed login attempts, it moves to collect evidence — capturing an image of the person attempting unauthorized access and the time of the access:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /home/kene/Project/DevOps
fswebcam snapshot.jpg &gt; fswebcam.log 2&gt;&amp;1
TIMESTAMP=$(date <span class="hljs-string">'+%Y-%m-%d %H:%M:%S'</span>)
</code></pre>
<h3 id="heading-breakdown-of-the-snapshot-process">Breakdown of the Snapshot process:</h3>
<ul>
<li><p><code>cd /home/kene/Project/DevOps</code>: Change directory to the folder snapshots are to be saved. This ensures that the snapshot and webcam logs are saved in a centralized location for easy access.</p>
</li>
<li><p><code>fswebcam snapshot.jpg</code>: This prompts the <code>fswebcam</code> utility to take a photo using the system’s default webcam. The Image is then saved with name as defined in the Script.</p>
</li>
<li><p><code>&gt; fswebcam.log 2&gt;&amp;1</code>: This redirects both the Standard Output (stdout) and Standard Error (stderr) to <code>fswebcam.log</code> file.</p>
</li>
<li><p><code>TIMESTAMP</code>: This is used to get the exact date and time the snapshot was taken. It saves the initial time and date for when the Owner receives the email incase the internet is out at that time.</p>
</li>
</ul>
<hr />
<h3 id="heading-step-8-email-the-alert-with-attachment">Step 8: Email the Alert with Attachment</h3>
<p>Once the attacker’s image has been captured and saved, the next step is to notify the system owner via email. This is handled using the <code>on_internet_connected</code> function:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">on_internet_connected</span></span>() {
    (<span class="hljs-built_in">echo</span> <span class="hljs-string">"Subject: URGENT SECURITY ALERT"</span>;
     <span class="hljs-built_in">echo</span> <span class="hljs-string">"We have detected 3 consecutive login failures on your system, which indicates an unauthorized access attempt by this attacker. The attacker, whose identity has been attached to the mail, attempted to breach your system at"</span>; 
     <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$TIMESTAMP</span>"</span>
     uuencode /home/kene/Project/DevOps/snapshot.jpg snapshot.jpg) | msmtp okekerichard281@gmail.com
}
</code></pre>
<h3 id="heading-breakdown-of-the-email-notification-process">Breakdown of the Email Notification process:</h3>
<ul>
<li><p><strong>Email Subject and Body</strong>: Uses <code>echo</code> to craft the email's subject and body. It provides clear context that an intrusion attempt was detected and that visual evidence has been attached.</p>
</li>
<li><p><code>$TIMESTAMP</code>: Appends the exact date and time the alert was triggered. This makes the alert more traceable for the system owner.</p>
</li>
<li><p><code>uuencode</code>: Converts the image file into an email-friendly format so it can be attached as <code>snapshot.jpg</code>.</p>
</li>
<li><p><code>msmtp</code>: A lightweight SMTP client used to send the actual email. It forwards the entire message to the configured email address.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743498141498/ae43ecda-b4e7-4cf9-99fb-77e4135dec42.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<hr />
<h3 id="heading-step-9-configuring-msmtp-file">Step 9: Configuring <code>msmtp</code> file</h3>
<p>Properly configure the <code>msmtp</code> file so that the script can successfully send emails.</p>
<p>Note that <code>msmtp</code> reads settings from a config file, typically located at <code>~/.msmtprc</code>.</p>
<p>Here’s a basic example configuration for Gmail:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.msmtprc</span>
account        gmail
host           smtp.gmail.com
port           587
from           your-email@gmail.com
user           your-email@gmail.com
password       abcd efgh ijkl mnop  <span class="hljs-comment"># Replace this with your App Password</span>
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        ~/.msmtp.log
</code></pre>
<h3 id="heading-key-fields-explained">Key Fields Explained:</h3>
<ul>
<li><p><code>account</code>: Defines the owner’s account to use when sending mail.</p>
</li>
<li><p><code>host</code>: The SMTP server of your mail provider.</p>
</li>
<li><p><code>port</code>: SMTP port (usually 587 for Transport Layer Security).</p>
</li>
<li><p><code>user</code> and <code>from</code>: Add sender email address.</p>
</li>
<li><p><code>password</code>: A 16-character password is used instead of the real password in the <code>msmtprc</code> file.</p>
</li>
<li><p><code>tls</code> and <code>tls_trust_file</code>: Ensures the email is securely transmitted.</p>
</li>
</ul>
<p>To automate the Security alert Email process, configure msmtp to work with Gmail. However, due to Google's security restrictions, direct password authentication was not allowed. To work around this, generate an App Password, which would allow msmtp to send emails securely.</p>
<p>To generate an App Password;</p>
<ul>
<li><p>navigate to Google Account ==&gt; Security</p>
</li>
<li><p>Scroll down to "Signing in to Google"</p>
</li>
<li><p>Ensure that 2-Step Verification is enabled. This is a requirement for successfully generating an App Password.</p>
</li>
<li><p>If 2-Step Verification is successfully enabled, proceed to the Google App Passwords page.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743531144522/7d83e4d3-fbdb-48aa-bf0c-73daa5d45579.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Click "Generate" to get a 16-character App Password in a format like <code>abcd efgh ijkl mnop</code>.</p>
</li>
<li><p>Carefully copy this password and integrate it into the <code>msmtprc</code> configuration file. Now, the system can send emails securely without using the actual Gmail password.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743480851636/dd190fa5-46ca-4a0b-ac27-154c2f8d4362.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-10-waiting-for-internet-before-sending-alert">Step 10: Waiting for Internet Before Sending Alert</h3>
<p>To ensure that the email can be sent successfully, the script checks for internet connectivity before executing the <code>on_internet_connected</code> function:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">check_internet</span></span>() {
    ping -c 1 google.com &amp;&gt; /dev/null
}

<span class="hljs-keyword">while</span> <span class="hljs-literal">true</span>; <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">if</span> check_internet; <span class="hljs-keyword">then</span>
        on_internet_connected
        <span class="hljs-built_in">break</span>
    <span class="hljs-keyword">fi</span>
    sleep 10
<span class="hljs-keyword">fi</span>
</code></pre>
<h3 id="heading-breakdown-of-the-connection-process">Breakdown of the Connection process:</h3>
<ul>
<li><p><code>check_internet()</code>: This function attempts to ping Google once (<code>-c 1</code>). The <code>&amp;&gt; /dev/null</code> redirects both stdout and stderr to <code>/dev/null</code>, effectively silencing the command's output. If the ping is successful, the function returns 0, which Bash treats as <code>true</code>.</p>
</li>
<li><p><code>while true; do ... done</code>: This loop runs endlessly until <code>check_internet</code> returns true, i.e. an internet connection is detected.</p>
</li>
<li><p><code>sleep 10</code> : This prevents the loop from overwhelming the system by adding a 10-second pause between each connectivity check.</p>
</li>
<li><p><code>on_internet_connected</code>: This is called once a connection is confirmed, this function handles sending the alert email with the snapshot.</p>
</li>
</ul>
<hr />
<h2 id="heading-scheduling-with-cron">Scheduling with Cron</h2>
<p>Once the script is fully functional, the next step is to <strong>automate its execution</strong>. This ensures it starts running without requiring manual intervention — especially after reboot.</p>
<h3 id="heading-configure-cron-for-the-root-user">Configure <code>cron</code> for the root user</h3>
<p>Because the script needs access to <code>/var/log/auth.log</code> (which requires root privileges), Edit the root user’s crontab:</p>
<pre><code class="lang-bash">sudo crontab -e
</code></pre>
<p>Add the path to run the script file at system startup</p>
<pre><code class="lang-bash">@reboot path-to-script-file
<span class="hljs-comment"># Example @reboot /home/kene/Project/DevOps/Scripts/login-monitor.sh</span>
</code></pre>
<hr />
<h2 id="heading-errors-encountered-and-how-i-handled-them">Errors Encountered and how I Handled them</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Error Encountered</strong></td><td><strong>How it was Handled</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Webcam not detected</td><td>Checked device permissions</td></tr>
<tr>
<td>Email not send due to SMTP failure</td><td>Checked the <code>.msmtprc</code> file configuration and internet access</td></tr>
<tr>
<td>Permission denied when trying to access log file</td><td>Used <code>sudo</code> and adjusted group permissions</td></tr>
<tr>
<td>Script not running because it was not executable</td><td>Used <code>chmod +x</code> <a target="_blank" href="http://login-monitor.sh"><code>login-monitor.sh</code></a></td></tr>
<tr>
<td>Cron not executing due to wrong path.</td><td>Used the absolute path</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>This project showcases how Bash scripting can be used to build a real-time security alert system. By monitoring system logs, tracking log-in behavior, taking snapshots with a webcam, and sending automated alerts, I gained practical experience in Bash scripting, Linux security, and automation.</p>
<p>If you found this guide helpful or want to make a contribution, feel free to reach out via:</p>
<ol>
<li><p><a target="_blank" href="https://github.com/RichardBenjamin/login-monitor-alert/">View the GitHub Repository</a></p>
</li>
<li><p><a target="_blank" href="http://linkedin.com/in/kenechukwu-okeke-295397290">Connect with me on LinkedI</a>n</p>
</li>
<li><p><a target="_blank" href="https://x.com/kenechukwu6673">Connect with me on X</a></p>
</li>
</ol>
]]></content:encoded></item></channel></rss>