And putting it online for the world
The other day I wrote about how I improved my wordle solver using Bash scripting. This was a fun project that I did mainly just to see if I could. I said that I was overall happy with the script but knew it could be improved. So that’s what I set out to do over the last few days.
The script worked but there was one section of the script I wasn’t happy with.
... echo "Are there any letters you know exist, but not the place? (Y/N)" read answer if [ $answer == Y ]; then echo "How many letters? (Enter 1-5)" read number_letters if [ $number_letters = 5 ]; then echo "Letter One" read letterA echo "Letter two" read letterB echo "Letter three" read letterC echo "Letter Four" read letterD echo "Letter Five" read letterE grep -Ei "([$letterA])" ./wordleguesses2.txt | grep -Ei "([$letterB])" | grep -Ei "([$letterC])" | grep -Ei "([$letterD])" | grep -Ei "([$letterE])" > ./wordleguesses.txt elif [ $number_letters = 4 ]; then echo "Letter One" read letterA echo "Letter Two" ...
This was bad code, and I knew it was bad code when I wrote it. While functional for the purpose I needed it was not optimized, relying on a series of if-this-then-that statements. From a computing standpoint this is less ideal as well since the computer has to make several checks until it can find the right number that was input. If a user entered the number 2 then the system first has to check if they entered 5, then check if they entered 4, then 3, then 2.
In terms of computing time this only adds tiny fractions of a second but in terms of programming time it adds quite a bit more. I programmed this entire thing in nano so I had to type out every line without copy and paste. For five variables that wasn’t too bad, we’re talking about 40 lines of code, but what if it was accepting inputs up to 10, or 100? Are we now talking about 80 lines of code? 800 Lines of code? This could grow exponentially. How many errors would I have in that 800 lines of code?
Again, I want to stress that quick and dirty coding is ok when you are just trying to make something work, but if you’re going to roll something out to production, you’ll need it to work properly. So, I took some time to rewrite that section.
echo "Are there any letters you know exist, but not the place? (Y/N)" read answer if [ $answer == Y ]; then cp /tmp/wordleguesses.txt /tmp/words echo "How many letters? (Enter 1-5)" read i while [ $i -gt 0 ]; do echo "What is letter $i" read letter grep -Ei "([$letter])" /tmp/words >> /tmp/wordleguesses2.txt mv /tmp/wordleguesses2.txt /tmp/words cp /tmp/words /tmp/wordleguesses.txt ((i--)) done fi
This is my improved code. Instead of having 40 lines of repeating code I streamlined it to 10; reducing that code section by 75% and the code in the overall script by 30%. Now when a user enters a number the script will iterate through a loop, decrementing by one until it reaches zero. It passes the letter through grep to pick only words that have whatever letter the user specifies, then goes back through and selects words that have the next letter, etc, until it reaches zero. It writes all of that to a text file then renames that text file to be the input for the next guess.
While not important for solving Wordle this code also scales to meet larger numbers, like say 100 inputs. Now I know I’m not coming up with any groundbreaking coding here; while loops have existed almost as long as programming has, I’m simply explaining for any non-programmers why a while loop like this is better than doing it manually.
In addition to the above change, I also took a moment to make the script more accessible to everyone. I use Zorin as my OS for a lot of things. I like its overall theming support and being based on Ubuntu I am much more familiar with it; however, I realize that not everyone uses Zorin, and therefore not everyone has the default Zorin files, particularly the dictionary list I was using in my script. Zorin includes some nonstandard dictionary lists which I was using.
To make sure the script can be downloaded and run immediately I needed to include a dictionary file, which meant I needed several things.
- I needed a link to a readily available dictionary list
- I needed to store this dictionary somewhere
- I needed a way to check if the file was already there so the user wasn’t downloading it every time they ran the script.
This is what I came up with.
user=$(whoami) echo "Welcome to the Wordle Solver." echo "==========================================" echo "Checking Prerequisites. One moment." if [ -f /home/$user/wordlewords ]; then echo "Dictonary Found. Proceeding with program." echo "=========================================" else echo "Downloading file." wget -q --show-progress https://raw.githubusercontent.com/tabatkins/wordle-list/main/words mv words /home/$user/wordlewords echo "Dictonary downloaded. Proceefing with program." echo "==============================================" fi
Another fairly simple block of code. Get the username of the user and store that in a variable. Check if the file wordlewords exists in that user’s home directory. If it does then move on, if it doesn’t then download the file.
The overall script is now 69 lines, down from 102 total, and could be reduced further. I could cut out most of this section and as it’s simply informing the user what’s happening but I’m ok with 69 lines of code.
Enabling the script for wider access
The script was only one half of the equation. I also wanted to provide my script to my coworkers but they’re not very tech savvy people, so asking them to run a script on a Linux machine was going to be a no-go. The simplest solution without going through the hassle of trying to turn this bash script into an executable was to make it interactive on a web browser.
Cloudflare has an option to allow SSH access through a web browser but that requires setting up a Cloudflare Access rule. I like Cloudflare Access, and I recommended it in my securing Pi-Hole series, but I wasn’t going to ask all my coworkers for their emails for the access groups, and my work has too many IPs (at least 1 /8) for me to add an IP ACL. So that was out. I needed something else.
So, I stood up a simple VM and a Guacamole server. I made a user account and set it’s only connection to SSH into my VM so whenever that user logins in they are immediately logged into the VM with the wordlesolver script. I configure the Guacamole Nginx proxy_pass to pass the username and password variable as URI arguments to autologin users when they hit the website.
You might be reading this and saying “wait a second you put a VM out on the internet and made it publicly accessible? That’s a terrible idea!” And you’re right, it is a terrible idea. That’s why I took steps to secure it.
Securing the environment
I had the same thought that putting a putting a VM out on the internet would be a bad idea. Someone could use my VM to hack the rest of my network, they could use it for crypto mining, staging servers, downloading malware, etc. I needed to deal with all of those. So, here’s what I did.
On the VM
On the VM I took several steps to lock down the VM. First, I created a new non-Sudo user. This user would be who people logged into, so I needed to enable SSH access for them. I modified SSH config file to disable root login and only allow the wordle user. This was an important step but someone could still login to the wordle user and then SU into root. This was obviously not good because as root they could do whatever they wanted.
To prevent SU access, I had to modify the PAM file to disable SU all together. Now the only way to access root is to login as root from the terminal, otherwise you get a permission denied error. This effectively locks the users in the wordle user without Sudo access; however it doesn’t prevent them from downloading malware or trying to perform other hacks of my network.
To prevent this, I used UFW and set the default policy to deny all outbound, deny all inbound, allow port 22 inbound from the Guacamole IP. This effectively stops all networking capabilities of the host while still allowing SSH access in. The Wordle user is a non Sudo user, so they don’t have the ability to modify UFW and now they also can’t try brute forcing SU. I also went one step further and blocked all network connections from that IP in my pfSense.
Finally, Guacamole has a few interesting settings for the connection. I disabled copy and paste from terminal and client to prevent someone from passing a program over in hex (a trick I’ve done in a CTF). I disabled downloading/uploading files through Guacamole to the VM. Perhaps the neatest trick with Guacamole however is the execution command flag. Guacamole can pass a custom command on SSH login to the VM. I used this feature to automatically run the wordlesolver script on login; however, it had an added benefit I didn’t realize at first. It locks the user into the script. If the user attempts to terminate the script with ctrl + c the SSH session is also terminated.
On the Network layer
For Guacamole I disabled the default admin account and made a new one. This admin account has a 68 character randomly generated password including caps and special characters, non-SMS based 2FA, time limited access. In addition to this the guacamole server only accepts unique SSH key login with a passphrase and TOTP MFA from an isolated system in a separate management VLAN.
The Guacamole Server exists on an isolated port on the switch that only allows comms from the Guacamole to the VM. This Guacamole server is not used for and does not connect to any other machines on the network.
Entering the network passes through Cloudflare’s proxy and WAF which blocks most web attacks. In the WAF I have Geo filtering to block common spammer countries. My firewall only accepts connections from Cloudflare.
Everything in action
With everything set up this is what the program looks like: visiting https://wordlesolver.gravitywall.net will take you to the solver in action. I won’t leave this website forever and if I do, I’ll eventually move it over to something like GCP or Azure.
As with everything I had a ton of fun designing, building, and securing this.