SSH login monitor (Part 2)
This story began as a casual conversation about SSH keys. See the beginning in Part 1.
Create a fingerprint database
Fingerprints are only useful if you have collected a good database of them. This is what I did after receiving the emails from my users.
On the lab host (rhel-lab
) I saved the users' public keys in a separate directory under /root
. Of course, I made it readable only by root.
# mkdir ~/ssh-keys
# chmod 0700 ~/ssh-keys
# cd ~/ssh-keys
I copied the users’ public keys they sent me here and added the owner’s name to each file.
# echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8Obx1FsUu1jlYDtzfEDHYSDjG82xE7ysxZVzhgpGC5 alice@fedora" > alice_id_ed25519.pub
# echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgclT4eQ5RlYabZfkdjFV5wGrroXxmd5n2X7okmiaN8 bob@fedora" > bob_id_ed25519.pub
# echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJWcjljox2NKwDFllZ5KQc4LSVrBEKoaOE/t/up1XbyD charlie@fedora" > charlie_id_ed25519.pub
# ls -l *pub
-rw-r--r--. 1 root root 94 Apr 27 09:53 alice_id_ed25519.pub
-rw-r--r--. 1 root root 92 Apr 27 09:54 bob_id_ed25519.pub
-rw-r--r--. 1 root root 96 Apr 27 09:54 charlie_id_ed25519.pub
Then I ran the following command against each public key file to create its fingerprint.
# ssh-keygen -lf alice_id_ed25519.pub
256 SHA256:5xuxPx8QnPv19/6IZ5frmQj1N0hRCP9J364ddE6avL8 alice@fedora (ED25519)
# ssh-keygen -lf bob_id_ed25519.pub
256 SHA256:is6l6bRqCCBVKunT+zVGHoUF0A06p8lt/04EoRbyCUY bob@fedora (ED25519)
# ssh-keygen -lf charlie_id_ed25519.pub
256 SHA256:QgAov0UZI25hWxnbLiHa00j64/zD1m80UMsSIZtxr2s charlie@fedora (ED25519)
In the same directory, I opened a file called users.csv
and added three records in the form of username,fingerprint
, like this:
alice,5xuxPx8QnPv19/6IZ5frmQj1N0hRCP9J364ddE6avL8
bob,is6l6bRqCCBVKunT+zVGHoUF0A06p8lt/04EoRbyCUY
charlie,QgAov0UZI25hWxnbLiHa00j64/zD1m80UMsSIZtxr2s
Now I needed a program to scan the /var/log/secure
file, find login and logout messages, parse them to find the fingerprint, and look up the user based on their fingerprint in the database.
Create a log-monitoring application
I started learning Go recently, so for each new idea I use Go to practice. So this problem looked like a good exercise.
The program’s logic is pretty simple:
- Scan the log file and create a list of login/logout events.
- For each login event, find the user based on their fingerprint.
- Create a list of sessions and add login events to it.
- For each logout event, find the corresponding login event based on the source IP and the port and update the end time of the session.
- Output all sessions with user names, source IPs, start/end times, and duration.
The most challenging part was to parse the log file and collect all necessary fields. That’s why the regular expressions might look scary.
I created a simple Go program consisting of a single main.go
file and tested it on a short fragment of /var/log/secure
file. It printed out this:
# go run main.go
alice 192.168.1.24 2023-04-27 10:21:19 2023-04-27 10:21:22 3s
bob 192.168.1.24 2023-04-27 10:21:34 2023-04-27 10:21:37 3s
charlie 192.168.1.24 2023-04-27 10:21:55 2023-04-27 10:21:58 3s
Use AI to improve the application
The first version of this app was a simple main.go
file with hard-coded file names. I was playing around and needed a simple demo. My first improvement was adding the command-line arguments. I added the pflag
package (https://pkg.go.dev/github.com/spf13/pflag) and turned on Codeium (https://codeium.com/) in my VS Code. And here, AI began to help me.
AI coding assistants are very impressive, no doubt. But it’s one thing when you see it helping somebody in the video or you’re trying it yourself with some example programs. And it’s another thing when you write something yourself, you work on your own project, and it starts really helping you. Then you can clearly see how much time you saved by not typing a lot of things (just press [Tab] to accept!), by not looking around your own code (what should be included in this struct
, I forgot?), and by not googling function library definitions and arguments. AI remembers all this for you.
Back to my code. I just started typing userDB := flag.
and Codeium already knew that it should be StringP
and the argument should be named users
(short form is u
) and the reasonable default should be users.csv
. I didn’t argue and accepted. The next argument was the same: I added the log
argument almost without typing anything.
So far, so good. Let’s try another tool. I opened ChatGPT and asked:
Me: Act as a Go programming mentor. I will give you a program I wrote. Please suggest possible tests to add to this program. Here is my program:
…and I pasted my simple main.go
in the chat window.
In the answer, it suggested several cases that I have to test with each function: valid input, empty input, invalid input, duplicate fingerprints, etc. In the end, ChatGPT gave me an example of how it can be done and added:
AI: You can follow a similar pattern to write tests for the other functions as well.
Wow, it acted like a real mentor! It didn’t write the code for me, but it helped me to move in the right direction.
I wanted to write my tests the right way and played a role of a good student:
Me: I read an article that suggested keeping the main.go
file small and let the main function only call the application function. They suggested having other functions in separate files and argued that it helps in testing. Can you help me to apply these suggestions to my code?
“Sure!” the AI answered and suggested a good plan of moving all my functions to a separate pkg/sshloginmonitor
directory and creating files user.go
, session.go
, and util.go
.
I followed the suggestion, and our discussion continued.
Me: My program should log a fatal error under certain conditions. How should I test that?
In the answer, it explained that it’s possible, but I should keep in mind that the call to log.Fatal()
will terminate my test.
Me: Right! I shouldn’t call log.Fatal()
from the function. I should return an error instead. How should I check if the error is returned?
The AI gave me a full explanation with an example of how it should be done.
Me: How should I specify the expected error in the lists of tests?
Another great example with a slice of test cases showing how to specify the expected error.
Me: How should I test reading from a file? Can it be done by reading from a string constant?
Another great suggestion from AI: you probably should pass io.Reader
to your function, not a file name. That way, it will be much easier to test. Accepted; I re-wrote my functions to use io.Reader
instead of file names.
And so on, and so forth. Step by step, with the help of ChatGPT and Codeium, my little program got the tests it needed, docstrings for functions, and test cases for different conditions. In other words, in just a couple of hours, it looked much more professional.
I don’t know if AI can fully replace programmers. But I’m sure it can help us write better code. Just don’t be afraid and ask questions.
Find the code in this repo https://github.com/pavelanni/ssh-login-monitor
_____
“Wait,” I thought. “What if I give the AI the full description of my problem? Will it be able to write it from scratch?”
To be honest, I was a bit skeptical. ChatGPT has impressed me already, helping with my code here and there. But to solve this problem from scratch, just from the problem description? Probably not. But let’s give it a try.
The story continues in Part 3.