<?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: Ardiansyah Sulistyo</title>
    <description>The latest articles on DEV Community by Ardiansyah Sulistyo (@aardnsyhs).</description>
    <link>https://dev.to/aardnsyhs</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%2F3782117%2Fc2c99641-0f5d-4e36-a633-e1b8f9436406.jpg</url>
      <title>DEV Community: Ardiansyah Sulistyo</title>
      <link>https://dev.to/aardnsyhs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aardnsyhs"/>
    <language>en</language>
    <item>
      <title>Get a Telegram Notification Every Time Someone SSHes Into Your Server</title>
      <dc:creator>Ardiansyah Sulistyo</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:23:32 +0000</pubDate>
      <link>https://dev.to/aardnsyhs/get-a-telegram-notification-every-time-someone-sshes-into-your-server-2pji</link>
      <guid>https://dev.to/aardnsyhs/get-a-telegram-notification-every-time-someone-sshes-into-your-server-2pji</guid>
      <description>&lt;p&gt;A fresh VPS starts receiving SSH brute-force attempts within minutes of&lt;br&gt;
going public. Most of us find out about a breach &lt;em&gt;after&lt;/em&gt; the damage -&lt;br&gt;
a spike in outbound traffic, a suspended hosting account, a client asking&lt;br&gt;
why their site is serving spam.&lt;/p&gt;

&lt;p&gt;What if you just... knew? The second someone authenticates?&lt;/p&gt;

&lt;p&gt;Here's a complete, working setup that sends a Telegram message to your&lt;br&gt;
phone every time a user successfully logs in over SSH - key or password,&lt;br&gt;
interactive or automated. It's about 30 lines of Bash plus one line of PAM&lt;br&gt;
config. No agent, no daemon, no third-party service beyond Telegram's free&lt;br&gt;
Bot API.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;SSH login events pass through &lt;strong&gt;PAM&lt;/strong&gt; (Pluggable Authentication Modules)&lt;br&gt;
on Linux. PAM has a module called &lt;code&gt;pam_exec&lt;/code&gt; that can run an arbitrary&lt;br&gt;
script at specific points in the auth lifecycle - including&lt;br&gt;
&lt;code&gt;open_session&lt;/code&gt;, which fires only &lt;em&gt;after&lt;/em&gt; a successful login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/pam.d/sshd
  └─ pam_exec (open_session) ──▶ /usr/local/bin/ssh-notify
                                       └─ curl → Telegram Bot API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire architecture. No polling, no log-tailing, no extra&lt;br&gt;
processes running in the background.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 - Create a Telegram bot (2 minutes)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open Telegram, search for &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Send &lt;code&gt;/newbot&lt;/code&gt;, follow the prompts, name it whatever you like&lt;/li&gt;
&lt;li&gt;Copy the token it gives you - looks like &lt;code&gt;123456789:ABCdefGHIjklMNOpqrSTUvwxYZ&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now get your chat ID:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send any message to your new bot&lt;/li&gt;
&lt;li&gt;Visit &lt;code&gt;https://api.telegram.org/bot&amp;lt;YOUR_TOKEN&amp;gt;/getUpdates&lt;/code&gt; in a browser&lt;/li&gt;
&lt;li&gt;Find &lt;code&gt;"chat":{"id":YOUR_CHAT_ID}&lt;/code&gt; in the JSON response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it - no app registration, no OAuth flow, no approval process.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 - Store the credentials securely
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/serversecure
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/serversecure/telegram.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
BOT_TOKEN="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ"
CHAT_ID="987654321"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/serversecure/telegram.conf
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/serversecure/telegram.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Mode &lt;code&gt;600&lt;/code&gt;, owned by &lt;code&gt;root&lt;/code&gt; - the bot token is effectively a password.&lt;br&gt;
Never commit this file to version control.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 - The notification script
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;/usr/local/bin/ssh-notify&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ssh-notify - Telegram SSH login alert via pam_exec&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# CRITICAL: this script MUST exit 0 under all circumstances.&lt;/span&gt;
&lt;span class="c"&gt;# A non-zero exit code from a pam_exec hook can block the login.&lt;/span&gt;

&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;CONF_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/serversecure/telegram.conf"&lt;/span&gt;
&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;STATE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/ssh-notify"&lt;/span&gt;

main&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Only fire on successful login, not on session close&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAM_TYPE&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"open_session"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        return &lt;/span&gt;0
    &lt;span class="k"&gt;fi&lt;/span&gt;

    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONF_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0
    &lt;span class="c"&gt;# shellcheck source=/dev/null&lt;/span&gt;
    &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONF_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHAT_ID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0

    &lt;span class="c"&gt;# --- Rate limiting: collapse repeat alerts within 60s ---&lt;/span&gt;
    &lt;span class="c"&gt;# Tools like VS Code Remote SSH or rsync open multiple sessions per&lt;/span&gt;
    &lt;span class="c"&gt;# connection. Without this, one "login" becomes four notifications.&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAM_USER&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;unknown&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;rhost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAM_RHOST&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;unknown&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;key
    &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhost&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'[:alnum:]@.'&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;state_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STATE_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;state_file&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;last now elapsed
        &lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;state_file&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;elapsed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; now &lt;span class="o"&gt;-&lt;/span&gt; last &lt;span class="k"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;elapsed&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 60 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STATE_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
    &lt;span class="nb"&gt;date&lt;/span&gt; +%s &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;state_file&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null

    &lt;span class="c"&gt;# --- Build the alert ---&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;host ip
    &lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 3 https://ifconfig.me 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"🔐 *SSH Login Alert - &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;*

🖥️ Server: &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ip&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;)
👤 User: &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;
🌐 Source IP: &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhost&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;
🕒 Time: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S UTC'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="c"&gt;# --- Send it async, hard-timeout, never block the login ---&lt;/span&gt;
    &lt;span class="o"&gt;(&lt;/span&gt;
        setsid curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.telegram.org/bot&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sendMessage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"chat_id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHAT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"parse_mode=Markdown"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"text=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
    &lt;span class="o"&gt;)&lt;/span&gt; &amp;amp;

    &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="o"&gt;}&lt;/span&gt;

main
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /usr/local/bin/ssh-notify
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /usr/local/bin/ssh-notify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design decisions worth calling out, because they're the difference&lt;br&gt;
between "cool script" and "thing you can trust in production":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It always exits 0.&lt;/strong&gt; A pam_exec hook that returns non-zero can be
configured to block the login it's supposed to be reporting on. This
script fails silently, always, no exceptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The curl call runs in a detached subshell (&lt;code&gt;setsid ... &amp;amp;&lt;/code&gt;).&lt;/strong&gt; If
Telegram's API is slow or your network hiccups, your SSH login is never
delayed waiting on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting by IP+user.&lt;/strong&gt; Without this, tools that open multiple
SSH channels per "connection" (VS Code Remote is the worst offender)
will spam you with 3-4 notifications for what is, to you, one login.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4 - Wire it into PAM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"session    optional     pam_exec.so   seteuid /usr/local/bin/ssh-notify"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/pam.d/sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;optional&lt;/code&gt; control flag is important - it tells PAM this module's&lt;br&gt;
success or failure has no bearing on whether the login proceeds.&lt;/p&gt;

&lt;p&gt;Restart nothing - PAM config is read per-session, so your &lt;em&gt;next&lt;/em&gt; SSH login&lt;br&gt;
will trigger it. Test with a login from another terminal window; you&lt;br&gt;
should get a Telegram message within a couple of seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't cover (and what would)
&lt;/h2&gt;

&lt;p&gt;This gets you real-time visibility. It doesn't get you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geolocation on the source IP&lt;/strong&gt; - doable with a free IP lookup API,
left out here to avoid a third-party dependency by default (and for
privacy - you're now sending login IPs to another service).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"New device" flagging&lt;/strong&gt; - tracking previously-seen IPs and flagging
first-time sources needs a small persistent IP list, easy to add.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail2Ban ban alerts, sudo usage alerts, disk-full alerts&lt;/strong&gt; - same
pattern, different trigger. PAM's &lt;code&gt;pam_exec&lt;/code&gt; only covers auth events;
these need to hook into &lt;code&gt;sudo&lt;/code&gt;'s own PAM stack or a cron check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want all of that plus the rest of the hardening that should happen&lt;br&gt;
&lt;em&gt;before&lt;/em&gt; you ever expose SSH to the internet - non-root user setup, UFW,&lt;br&gt;
Fail2Ban jails, automatic SSL - that's the larger tool this is one module&lt;br&gt;
of: &lt;a href="https://aardnsyhs.gumroad.com/l/serversecure" rel="noopener noreferrer"&gt;ServerSecure Setup&lt;/a&gt;. This snippet is the full&lt;br&gt;
signature feature, free, because it costs me nothing to give away and it's&lt;br&gt;
the best proof I can offer that the rest of the tool is built with the&lt;br&gt;
same care.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or found an edge case? Drop a comment - I read and reply to&lt;br&gt;
every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>bash</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why Most Cafe Websites Feel Outdated (And How I Fixed It)</title>
      <dc:creator>Ardiansyah Sulistyo</dc:creator>
      <pubDate>Tue, 19 May 2026 08:09:52 +0000</pubDate>
      <link>https://dev.to/aardnsyhs/why-most-cafe-websites-feel-outdated-and-how-i-fixed-it-im2</link>
      <guid>https://dev.to/aardnsyhs/why-most-cafe-websites-feel-outdated-and-how-i-fixed-it-im2</guid>
      <description>&lt;p&gt;Most cafe websites still look like generic business templates from 2016.&lt;/p&gt;

&lt;p&gt;Bright backgrounds, cluttered layouts, autoplay sliders, weak typography, and mobile experiences that feel like an afterthought.&lt;/p&gt;

&lt;p&gt;I wanted to create something that feels modern, cinematic, and mobile-first — more like a premium app experience than a traditional website.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Noir &amp;amp; Brew&lt;/strong&gt;, a premium Astro landing page template for modern coffee shops and creative F&amp;amp;B brands.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Problem With Most Cafe Websites
&lt;/h1&gt;

&lt;p&gt;Most cafe websites follow the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic layouts&lt;/li&gt;
&lt;li&gt;Poor mobile navigation&lt;/li&gt;
&lt;li&gt;Weak visual hierarchy&lt;/li&gt;
&lt;li&gt;Overcrowded sections&lt;/li&gt;
&lt;li&gt;Inconsistent branding&lt;/li&gt;
&lt;li&gt;Slow performance&lt;/li&gt;
&lt;li&gt;Outdated UI patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;They don’t feel memorable.&lt;/p&gt;

&lt;p&gt;And for cafes, atmosphere matters just as much as the product itself.&lt;/p&gt;

&lt;p&gt;A website should communicate the same feeling customers get when they walk into the space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;warm lighting&lt;/li&gt;
&lt;li&gt;premium ambiance&lt;/li&gt;
&lt;li&gt;calm atmosphere&lt;/li&gt;
&lt;li&gt;intentional branding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That became the core direction behind this project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Designing Around Atmosphere
&lt;/h1&gt;

&lt;p&gt;Instead of focusing only on information density, I focused on visual mood and presentation first.&lt;/p&gt;

&lt;p&gt;The design direction was inspired by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editorial layouts&lt;/li&gt;
&lt;li&gt;luxury hospitality brands&lt;/li&gt;
&lt;li&gt;cinematic dark interfaces&lt;/li&gt;
&lt;li&gt;premium coffee packaging&lt;/li&gt;
&lt;li&gt;modern mobile app navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dark backgrounds&lt;/li&gt;
&lt;li&gt;warm gold accents&lt;/li&gt;
&lt;li&gt;oversized typography&lt;/li&gt;
&lt;li&gt;immersive cards&lt;/li&gt;
&lt;li&gt;spacious layouts&lt;/li&gt;
&lt;li&gt;minimal UI noise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make the website feel premium before users even start reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Mobile-First Was the Priority
&lt;/h1&gt;

&lt;p&gt;Most restaurant traffic comes from mobile devices.&lt;/p&gt;

&lt;p&gt;But many templates still treat mobile as a scaled-down desktop layout.&lt;/p&gt;

&lt;p&gt;I did the opposite.&lt;/p&gt;

&lt;p&gt;I designed the mobile experience first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bottom navigation&lt;/li&gt;
&lt;li&gt;app-like interaction&lt;/li&gt;
&lt;li&gt;compact content blocks&lt;/li&gt;
&lt;li&gt;thumb-friendly spacing&lt;/li&gt;
&lt;li&gt;immersive promo sections&lt;/li&gt;
&lt;li&gt;simplified menu browsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after the mobile structure felt complete did I expand the design into desktop.&lt;/p&gt;

&lt;p&gt;That decision alone made the experience feel significantly more modern.&lt;/p&gt;

&lt;h1&gt;
  
  
  Tech Stack
&lt;/h1&gt;

&lt;p&gt;I wanted the template to stay lightweight while still feeling interactive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack Used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Astro&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4&lt;/li&gt;
&lt;li&gt;Framer Motion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Astro?
&lt;/h2&gt;

&lt;p&gt;Astro gives excellent performance out of the box while still allowing interactive islands where needed.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast initial load&lt;/li&gt;
&lt;li&gt;less JavaScript shipped&lt;/li&gt;
&lt;li&gt;SEO-friendly pages&lt;/li&gt;
&lt;li&gt;smoother interactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For content-focused websites like cafes, portfolios, and hospitality brands, this architecture works extremely well.&lt;/p&gt;

&lt;h1&gt;
  
  
  Features
&lt;/h1&gt;

&lt;p&gt;The template currently includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsive landing pages&lt;/li&gt;
&lt;li&gt;Mobile-first layouts&lt;/li&gt;
&lt;li&gt;Interactive menu section&lt;/li&gt;
&lt;li&gt;Promo showcase&lt;/li&gt;
&lt;li&gt;Contact &amp;amp; location pages&lt;/li&gt;
&lt;li&gt;3 built-in themes&lt;/li&gt;
&lt;li&gt;SEO-ready structure&lt;/li&gt;
&lt;li&gt;Easy customization&lt;/li&gt;
&lt;li&gt;Production-ready codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Designed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cafes&lt;/li&gt;
&lt;li&gt;coffee shops&lt;/li&gt;
&lt;li&gt;restaurants&lt;/li&gt;
&lt;li&gt;hospitality brands&lt;/li&gt;
&lt;li&gt;creative studios&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Challenges During Development
&lt;/h1&gt;

&lt;p&gt;One of the hardest parts was balancing aesthetics with readability.&lt;/p&gt;

&lt;p&gt;Dark luxury interfaces can easily become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too low contrast&lt;/li&gt;
&lt;li&gt;visually noisy&lt;/li&gt;
&lt;li&gt;difficult to scan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had to simplify multiple sections repeatedly before the interface finally felt clean enough.&lt;/p&gt;

&lt;p&gt;Another challenge was avoiding over-animation.&lt;/p&gt;

&lt;p&gt;Modern websites often animate everything.&lt;/p&gt;

&lt;p&gt;Instead, I focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;subtle transitions&lt;/li&gt;
&lt;li&gt;layered depth&lt;/li&gt;
&lt;li&gt;hover feedback&lt;/li&gt;
&lt;li&gt;visual rhythm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That helped the UI feel calmer and more premium.&lt;/p&gt;

&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;I think websites for physical spaces should feel emotional — not just functional.&lt;/p&gt;

&lt;p&gt;Especially for cafes.&lt;/p&gt;

&lt;p&gt;People don’t only buy coffee.&lt;/p&gt;

&lt;p&gt;They buy atmosphere, identity, and experience.&lt;/p&gt;

&lt;p&gt;That’s what I tried to translate into this template.&lt;/p&gt;

&lt;h1&gt;
  
  
  Live Demo
&lt;/h1&gt;

&lt;p&gt;🔗 Demo:&lt;br&gt;
&lt;a href="https://cafe-template-sigma.vercel.app" rel="noopener noreferrer"&gt;Noir &amp;amp; Brew Demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 Gumroad:&lt;br&gt;
&lt;a href="https://aardnsyhs.gumroad.com/l/noir-and-brew-cafe-template" rel="noopener noreferrer"&gt;Noir &amp;amp; Brew Template&lt;/a&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>react</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a minimal company profile template because I was tired of colorful noise</title>
      <dc:creator>Ardiansyah Sulistyo</dc:creator>
      <pubDate>Fri, 20 Feb 2026 07:33:36 +0000</pubDate>
      <link>https://dev.to/aardnsyhs/i-built-a-minimal-company-profile-template-because-i-was-tired-of-colorful-noise-40b1</link>
      <guid>https://dev.to/aardnsyhs/i-built-a-minimal-company-profile-template-because-i-was-tired-of-colorful-noise-40b1</guid>
      <description>&lt;p&gt;🔗 &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://mono-profile-alpha.vercel.app/" rel="noopener noreferrer"&gt;https://mono-profile-alpha.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have a problem with most website templates.&lt;/p&gt;

&lt;p&gt;They're loud. Gradients everywhere, hero sections with three different&lt;br&gt;
typefaces, accent colors fighting each other. I get it they want to&lt;br&gt;
look impressive in a marketplace thumbnail.&lt;/p&gt;

&lt;p&gt;But when I actually need to &lt;em&gt;use&lt;/em&gt; one? It's exhausting.&lt;/p&gt;

&lt;p&gt;I like monochrome. Black, white, grays, maybe one muted accent.&lt;br&gt;
Clean type. Room to breathe. So after saying "I'll just build my own"&lt;br&gt;
for the fifth time, I actually did.&lt;/p&gt;

&lt;p&gt;This is MONO, a minimal company profile template I built for myself&lt;br&gt;
and ended up packaging for anyone else who thinks the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;I went with what I'm comfortable with, and what I'd actually want&lt;br&gt;
to inherit from someone else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; — fast dev server, no config hell&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React + TypeScript&lt;/strong&gt; — predictable, easy to extend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — utility-first, no surprise CSS specificity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn/ui&lt;/strong&gt; — accessible components without opinionated styling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shadcn was actually the right call here. Since the components are&lt;br&gt;
unstyled by default and live in your codebase, the monochrome&lt;br&gt;
aesthetic doesn't fight the component library. You're not overriding&lt;br&gt;
a blue primary button everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the template
&lt;/h2&gt;

&lt;p&gt;It's a typical company profile page, but intentionally stripped down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hero (headline, subtext, CTA)&lt;/li&gt;
&lt;li&gt;Services / what you offer&lt;/li&gt;
&lt;li&gt;Selected work / portfolio&lt;/li&gt;
&lt;li&gt;About section&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Contact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No unnecessary animations, no scroll-jacking, no&lt;br&gt;
"surprise me" interactions.&lt;/p&gt;

&lt;p&gt;The section order makes sense for most businesses: tell me who you&lt;br&gt;
are → what you do → proof you've done it → answer my obvious&lt;br&gt;
questions → let me reach you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision I'm most glad I made
&lt;/h2&gt;

&lt;p&gt;I almost added a color theme switcher. "Users can pick an accent&lt;br&gt;
color!" Sounds useful, right?&lt;/p&gt;

&lt;p&gt;I removed it.&lt;/p&gt;

&lt;p&gt;Not because it's hard to build it's not. But it introduces&lt;br&gt;
decisions. "Should I use purple or teal?" is exactly the kind of&lt;br&gt;
question I want the template to eliminate, not create.&lt;/p&gt;

&lt;p&gt;Monochrome is a decision. Stick to it.&lt;/p&gt;

&lt;p&gt;If you want a specific accent later, you change one Tailwind CSS&lt;br&gt;
variable and you're done. That's intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Honestly? Anyone who needs a company profile that looks professional&lt;br&gt;
without spending a week on design decisions.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agencies putting together a quick client-facing profile&lt;/li&gt;
&lt;li&gt;Freelancers who want a site that looks serious&lt;/li&gt;
&lt;li&gt;Developers who'd rather write code than pick color palettes&lt;/li&gt;
&lt;li&gt;Someone building a startup landing page before they figure out branding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a dashboard, not a SaaS template, not a landing page with&lt;br&gt;
17 sections. Just a company profile. Done well.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to grab it
&lt;/h2&gt;

&lt;p&gt;It's on Gumroad: &lt;strong&gt;[&lt;a href="https://aardnsyhs.gumroad.com/l/mono-company-profile" rel="noopener noreferrer"&gt;https://aardnsyhs.gumroad.com/l/mono-company-profile&lt;/a&gt;]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comes with the full source code, clean folder structure, and a README&lt;br&gt;
that explains how to customize the key parts without touching&lt;br&gt;
everything.&lt;/p&gt;

&lt;p&gt;If you check it out or if you've built something similar I'd love&lt;br&gt;
to hear what you think. What's the one thing you always end up&lt;br&gt;
rebuilding when you use someone else's template?&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
