├── .gitignore ├── LICENSE ├── ez-ssh-bot-fail.sh ├── ez-ssh-bot-success.sh ├── ez-ssh-bot.png ├── ez-ssh-bot.service └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | misc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stefan Gränitz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ez-ssh-bot-fail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # We are subscribed to auth events 4 | if [ "$PAM_TYPE" == "auth" ]; then 5 | # Only monitor reverse-tunnel connections (they come in via local loopback) 6 | if [[ "$PAM_RHOST" == "127.0.0.1" || "$PAM_RHOST" == "::1" ]]; then 7 | CHAT_ID= 8 | BOT_TOKEN= 9 | message="$(date +"%Y-%m-%d, %A %R")"$'\n'"External SSH Login Failed: $PAM_USER@$(hostname)" 10 | curl -s --data "text=$message" --data "chat_id=$CHAT_ID" 'https://api.telegram.org/bot'$BOT_TOKEN'/sendMessage' > /dev/null 11 | fi 12 | fi 13 | -------------------------------------------------------------------------------- /ez-ssh-bot-success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # We are subscribed to session events and ignore closing ones 4 | if [ "$PAM_TYPE" != "close_session" ]; then 5 | # Only monitor reverse-tunnel connections (they come in via local loopback) 6 | if [[ "$PAM_RHOST" == "127.0.0.1" || "$PAM_RHOST" == "::1" ]]; then 7 | CHAT_ID= 8 | BOT_TOKEN= 9 | message="$(date +"%Y-%m-%d, %A %R")"$'\n'"External SSH Login: $PAM_USER@$(hostname)" 10 | curl -s --data "text=$message" --data "chat_id=$CHAT_ID" 'https://api.telegram.org/bot'$BOT_TOKEN'/sendMessage' > /dev/null 11 | fi 12 | fi 13 | -------------------------------------------------------------------------------- /ez-ssh-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echtzeit-dev/ez-ssh-bot/79516a50dc07c7efa3c6c8929a079536801b4b74/ez-ssh-bot.png -------------------------------------------------------------------------------- /ez-ssh-bot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SSH Reverse Tunnel with Login Notifications 3 | After=network-online.target 4 | 5 | [Service] 6 | Type=forking 7 | User=ez-ssh-bot 8 | ExecStart=/usr/bin/autossh -M 0 -N -f -q -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i ~/.ssh/ez-ssh-bot.pem -R 9033:localhost:22 ec2-user@ 9 | ExecStop=/usr/bin/pkill -9 -u ez-ssh-bot 10 | RestartSec=5 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ez-ssh-bot 2 | 3 | SSH Reverse Tunnel with Login Notifications via Telegram Bot 4 | 5 | ![Diagram](ez-ssh-bot.png) 6 | 7 | ## Create a Telegram bot 8 | 9 | We contact [@BotFather](https://t.me/botfather) on Telegram and type `/start`. Then we follow the instructions to create a bot and get an access token. The token will look like this: `4098346289:YUI_OLIJi98y78078yyhi7ovghjoTGniuss`. It grants full control over our bot, so we must keep it private. 10 | 11 | Now we have to create a new Telegram channel and add our bot as a member, so it can send messages to the channel. Let's post a "test" message from our own account and navigate to this page to find the channel ID: 12 | [https://api.telegram.org/bot<bot-access-token>/getUpdates](https://api.telegram.org/bot4098346289:YUI_OLIJi98y78078yyhi7ovghjoTGniuss/getUpdates). We find the channel ID in field `result/message/chat/id` of the returned JSON: 13 | 14 | ```json 15 | { 16 | "ok": true, 17 | "result": [ 18 | { 19 | "update_id": 108063316, 20 | "message": { 21 | "message_id": 1, 22 | "from": { 23 | "id": , 24 | "is_bot": false, 25 | "first_name": "Stefan", 26 | "username": "weliveindetail", 27 | "language_code": "en" 28 | }, 29 | "chat": { 30 | "id": , 31 | "first_name": "Stefan", 32 | "username": "weliveindetail", 33 | "type": "private" 34 | }, 35 | "date": 1667396395, 36 | "text": "test" 37 | } 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | We enter both, our bot-token and channel ID in the `ez-ssh-bot` scripts in this repo: 44 | ``` 45 | CHAT_ID= 46 | BOT_TOKEN= 47 | message="$(date +"%Y-%m-%d, %A %R")"$'\n'"External SSH Login Failed: $PAM_USER@$(hostname)" 48 | ``` 49 | 50 | We can test our bot like this: 51 | ```shell 52 | > PAM_RHOST=127.0.0.1 PAM_TYPE=open_session ./ez-ssh-bot-success.sh 53 | > PAM_RHOST=127.0.0.1 PAM_TYPE=auth ./ez-ssh-bot-fail.sh 54 | ``` 55 | 56 | In our Telegram channel we should receive two messages — one "External SSH Login" and one "External SSH Login Failed". Once that works, we copy the scripts to `/etc/ssh` and restrict access: 57 | ```shell 58 | > sudo cp ez-ssh-bot-*.sh /etc/ssh/. 59 | > sudo chown root /etc/ssh/ez-ssh-bot-*.sh 60 | > sudo chmod 100 /etc/ssh/ez-ssh-bot-*.sh 61 | ``` 62 | 63 | ## Set up a free AWS EC2 Instance 64 | 65 | This article guides through the configuration step by step — we must remember our elastic IP address and where we saved the `.pem` file from our key pair: https://www.opensourceforu.com/2021/09/how-to-do-reverse-tunnelling-with-the-amazon-ec2-instance/ 66 | 67 | ## Create a dedicated user for AutoSSH 68 | 69 | Let's create a dedicated user, set a password, copy over the `.pem` file for our EC2 instance and make sure the `known_hosts` file exists: 70 | ```shell 71 | > sudo useradd -m ez-ssh-bot 72 | > sudo passwd ez-ssh-bot 73 | > sudo mkdir -p /home/ez-ssh-bot/.ssh 74 | > sudo cp /path/to/private/ec2-key.pem /home/ez-ssh-bot/.ssh/ez-ssh-bot.pem 75 | > sudo chown ez-ssh-bot /home/ez-ssh-bot/.ssh/ez-ssh-bot.pem 76 | > sudo chmod 600 /home/ez-ssh-bot/.ssh/ez-ssh-bot.pem 77 | > sudo touch /home/ez-ssh-bot/.ssh/known_hosts 78 | > sudo chown ez-ssh-bot /home/ez-ssh-bot/.ssh/known_hosts 79 | ``` 80 | 81 | We switch to the new user once in order to test the SSH connection and confirm the server fingerprint. Here we need the elastic IP address of our EC2 instance: 82 | ```shell 83 | > su - ez-ssh-bot 84 | Password: ... 85 | ez-ssh-bot> ssh -i ~/.ssh/ez-ssh-bot.pem ec2-user@ 86 | The authenticity of host ' ()' can't be established. 87 | ECDSA key fingerprint is SHA256:VhApmMgDG00DVRlwAeFqmN3hDgtJZpvuvIV9Dy39gyk. 88 | Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 89 | [ec2-user@ip-172-31-43-51 ~]$ exit 90 | ez-ssh-bot> wc --chars /home/ez-ssh-bot/.ssh/known_hosts 91 | 221 /home/ez-ssh-bot/.ssh/known_hosts 92 | ez-ssh-bot> exit 93 | ``` 94 | 95 | Eventually, we can give the user a false shell to prevent further logins: 96 | ```shell 97 | > sudo usermod -s /usr/sbin/nologin ez-ssh-bot 98 | > su - ez-ssh-bot 99 | Password: ... 100 | This account is currently not available. 101 | ``` 102 | 103 | ## Create a Systemd service for AutoSSH 104 | 105 | We use AutoSSH to maintain the reverse SSH tunnel connection from our local workstation to the public EC2 instance. SSH connections to the respective port of the EC instance will then be forwarded to our local workstation through the reverse tunnel. We use Systemd to take care of starting AutoSSH after boot and restarting it in case of failures. 106 | 107 | First, we enter our elastic IP in the `ez-ssh-bot.service` in this repo and copy it to the Systemd system services folder: 108 | ```shell 109 | > grep "" ez-ssh-bot.service 110 | ExecStart=/usr/bin/autossh -M 0 -N -f -q -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i ~/.ssh/ez-ssh-bot.pem -R 9033:localhost:22 ec2-user@ 111 | 112 | > sudo cp ez-ssh-bot.service /etc/systemd/system/. 113 | ``` 114 | 115 | Let's make sure we have the `autossh` package installed. Then we reload all units from disk, start the service and check its status: 116 | ```shell 117 | > sudo apt install autossh 118 | > sudo systemctl daemon-reload 119 | > sudo systemctl start ez-ssh-bot 120 | > systemctl status ez-ssh-bot 121 | ● ez-ssh-bot.service - SSH Reverse Tunnel with Login Notifications 122 | Loaded: loaded (/etc/systemd/system/ez-ssh-bot.service; disabled; vendor preset: enabled) 123 | Active: active (running) since Wed 2022-11-02 13:23:42 CET; 13min ago 124 | Process: 2337703 ExecStart=/usr/bin/autossh -M 0 -N -f -q -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i ~/.ssh/ez-ssh-bot.pem -R 9033:localhost:22 ec2-user@ 125 | Main PID: 2337706 (autossh) 126 | Tasks: 2 (limit: 38185) 127 | Memory: 1.0M 128 | CGroup: /system.slice/ez-ssh-bot.service 129 | ├─2337706 /usr/lib/autossh/autossh -M 0 -N -q -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i ~/.ssh/ez-ssh-bot.pem -R 9033:localhost:22 ec2-user@ 130 | └─2337707 /usr/bin/ssh -N -q -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i ~/.ssh/ez-ssh-bot.pem -R 9033:localhost:22 ec2-user@ 131 | 132 | Nov 02 13:23:42 i7ubuntu systemd[1]: Starting SSH Reverse Tunnel with Login Notifications... 133 | Nov 02 13:23:42 i7ubuntu autossh[2337703]: port set to 0, monitoring disabled 134 | Nov 02 13:23:42 i7ubuntu autossh[2337706]: starting ssh (count 1) 135 | Nov 02 13:23:42 i7ubuntu systemd[1]: Started SSH Reverse Tunnel with Login Notifications. 136 | Nov 02 13:23:42 i7ubuntu autossh[2337706]: ssh child pid is 2337707 137 | ``` 138 | 139 | We can now SSH into the `user` account on our local workstation from a remote machine through our EC2 instance 🙌 140 | ```shell 141 | > ssh -p 9034 user@ 142 | ``` 143 | 144 | Once that works, let Systemd start our service automatically at boot-time: 145 | ```shell 146 | > sudo systemctl enable ez-ssh-bot 147 | ``` 148 | 149 | ## Add PAM steps to send notifications 150 | 151 | Let's connect the remaining pieces. 152 | The `/etc/ssh/ez-ssh-bot-success.sh` script sends a "External SSH Login" messsage for logins that originate from the reverse SSH tunnel. 153 | We want to run it whenever a login attempt succeeded. 154 | We can edit `/etc/pam.d/sshd` to achieve this: 155 | 156 | ```diff 157 | --- a/etc/pam.d/sshd 158 | +++ b/etc/pam.d/sshd 159 | @@ -27,6 +27,9 @@ session optional pam_keyinit.so force revoke 160 | # Standard Un*x session setup and teardown. 161 | @include common-session 162 | 163 | +# Send a login notification to Telegram via ez-ssh-bot 164 | +session optional pam_exec.so seteuid /etc/ssh/ez-ssh-bot-success.sh 165 | + 166 | # Print the message of the day upon successful login. 167 | # This includes a dynamically generated part from /run/motd.dynamic 168 | # and a static (admin-editable) part from /etc/motd. 169 | ``` 170 | 171 | The `/etc/ssh/ez-ssh-bot-fail.sh` script sends a "External SSH Login Failed" messsage for logins that originate from the reverse SSH tunnel. 172 | So, we want to run it whenever a login attempt failed. 173 | We can edit `/etc/pam.d/common-auth` to achieve this (and also add a 10 seconds delay for failed login attempts): 174 | 175 | ```diff 176 | --- a/etc/pam.d/common-auth 177 | +++ b/etc/pam.d/common-auth 178 | @@ -14,10 +14,12 @@ 179 | # pam-auth-update(8) for details. 180 | 181 | # here are the per-package modules (the "Primary" block) 182 | -auth [success=1 default=ignore] pam_unix.so nullok 183 | +auth [success=3 default=ignore] pam_unix.so nullok 184 | 185 | # here's the fallback if no module succeeds 186 | -auth requisite pam_deny.so 187 | +auth optional pam_exec.so seteuid /etc/ssh/ez-ssh-bot-fail.sh 188 | +auth optional pam_faildelay.so delay=10000000 189 | +auth requisite pam_deny.so 190 | 191 | # prime the stack with a positive return value if there isn't one already; 192 | # this avoids us returning an error just because nothing sets a success code 193 | ``` 194 | 195 | ## Voilà! 196 | 197 | External SSH Login | | External SSH Login Failed 198 | -------------------|---|--------------------------- 199 | ez-ssh-bot-success-demo | | ez-ssh-bot-fail-demo 200 | --------------------------------------------------------------------------------